From 3ce0d8b5a141ca4a2da4daaa60ce79875375c074 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 14 Nov 2024 12:55:01 +0100 Subject: [PATCH 001/103] added initial meta views --- app/commands/MetaTester.php | 41 +++++++ app/config/config.neon | 1 + app/model/view/MetaView.php | 229 ++++++++++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 app/commands/MetaTester.php create mode 100644 app/model/view/MetaView.php diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php new file mode 100644 index 000000000..5b440cc2d --- /dev/null +++ b/app/commands/MetaTester.php @@ -0,0 +1,41 @@ +setName(self::$defaultName)->setDescription( + 'Test the meta views.' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->test("a"); + return Command::SUCCESS; + } + + function test(string $arg) { + $view = new \App\Model\View\TestView(); + $view->endpoint([ + "id" => "0", + "organizational" => false, + ], "0001", true); + #$view->get_user_info(0); + } +} diff --git a/app/config/config.neon b/app/config/config.neon index 3f9e704eb..8fa380680 100644 --- a/app/config/config.neon +++ b/app/config/config.neon @@ -318,6 +318,7 @@ services: - App\Console\AsyncJobsUpkeep(%async.upkeep%) - App\Console\GeneralStatsNotification - App\Console\ExportDatabase + - App\Console\MetaTester - App\Console\GenerateSwagger - App\Console\SwaggerAnnotator - App\Console\CleanupLocalizedTexts diff --git a/app/model/view/MetaView.php b/app/model/view/MetaView.php new file mode 100644 index 000000000..4e6041bc7 --- /dev/null +++ b/app/model/view/MetaView.php @@ -0,0 +1,229 @@ +get_param_names_to_values_map($backtrace); + $params_to_format = AnnotationHelper::extractMethodCheckedParams($className, $methodName); + + foreach ($params_to_values as $param=>$value) { + $format = $params_to_format[$param]; + if (!$this->conforms_to_format($format, $value)) { + ///TODO: debug only + echo "Invalid param <$className:$methodName:$param> value <$value>, given format <$format> \n"; + } + } + } + + /// extracts function params and returns object representations of the formats + function validate_args() { + $backtrace = debug_backtrace()[1]; + $className = $backtrace['class']; + $methodName = $backtrace['function']; + $params_to_values = $this->get_param_names_to_values_map($backtrace); + $params_to_format = AnnotationHelper::extractMethodCheckedParams($className, $methodName); + + $a = new GroupFormat(); + AnnotationHelper::extractClassFormat(get_class($a)); + + foreach ($params_to_values as $param=>$value) { + $format = $params_to_format[$param]; + if (!$this->conforms_to_format($format, $value)) { + ///TODO: debug only + echo "Invalid param <$className:$methodName:$param> value <$value>, given format <$format> \n"; + } + } + } + + function get_param_names_to_values_map($backtrace) { + $className = $backtrace['class']; + $args = $backtrace['args']; + $methodName = $backtrace['function']; + + $class = new \ReflectionClass($className); + $method = $class->getMethod($methodName); + $params = array_map(fn($param) => $param->name, $method->getParameters()); + + $argMap = []; + for ($i = 0; $i < count($params); $i++) { + $argMap[$params[$i]] = $args[$i]; + } + return $argMap; + } +} + +class GroupFormat { + /** + * @format uuid + */ + public string $id; + /** + * @format uuid + */ + public string $externalId; + /** + * @format bool + * ///REDUNDANT + */ + public bool $organizational; + /** + * @format bool + */ + public bool $exam; + /** + * @format bool + */ + public bool $archived; + /** + * @format bool + */ + public bool $public; + /** + * @format bool + */ + public bool $directlyArchived; + /** + * @format localizedText[] + */ + public array $localizedTexts; + /** + * @format uuid[] + */ + public array $primaryAdminsIds; + /** + * @format uuid? + */ + public string $parentGroupId; + /** + * @format uuid[] + */ + public array $parentGroupsIds; + /** + * @format uuid[] + */ + public array $childGroups; + /** + * @format groupPrivateData + */ + public $privateData; + /** + * @format acl[] + */ + public array $permissionHints; +} + +class TestView extends MetaView { + //function endpoint() { generator(["user_info" /* this would generate the whole user_info object */, "messages":{"name", "message"} /* cherry picking */]) } + + + /// should formats be defined in comments, or in classes? + /// classes: enables autocomplete, enforces structure, creates a ton of data classes, classes are needed for all nested objects + /// comments: no class cluttering, less verbose, no autocomplete, does not enforce structure -> views are created as dictionaries + /** + * @format_def group { + * "id":"format:uuid", + * "externalId":"format:uuid", + * "organizational":"format:bool", + * "exam":"format:bool", + * "archived":"format:bool", + * "public":"format:bool", + * "directlyArchived":"format:bool", + * "localizedTexts":"format:localizedText[]", + * "primaryAdminsIds":"format:uuid[]", + * "parentGroupId":"format:uuid?", + * "parentGroupsIds":"format:uuid[]", + * "childGroups":"format:uuid[]", + * "privateData":"format:groupPrivateData", + * "permissionHints":"format:acl[]" + * } + */ + private $placeholder; + + // here the generator takes an input argument that conforms to format:user_info, therefore the generator can extract named parameters out + // of it and pass them to the database methods + ///TODO: check whether the parameters can support the below annotations with the current framework + /** + * Summary of endpoint + * @format_def user_info { "name":"format:name", "points":"format:int", "comments":"format:string[]" } + * @checked_param format:user_info user_info + * @checked_param format:uuid user_id + * @checked_param format:bool verbose + */ + private $old; + + + + /** + * @checked_param format:group group + * @checked_param format:uuid user_id + * @checked_param format:bool verbose + */ + function endpoint($group, $user_id, $verbose) { + $this->validate_args(); + + /*$data = [ + "id" => $user_id, + ]; + $this->generator($data, output_format: "format:group");*/ + + + //$message = $this->get_last_message($user_id); + //$this->generator(output_format: "format:messages[]:text"); + } + + // the names of the format and the output do not have to be identical, the strings in the desired data format refer the output names + /** + * @input format:user_id user_id // the input has to be invoked with the user_id extracted from format:group + * @format_def message { "id":"format:uuid", "name":"format:name", "text":"format:string", "date":"format:datetime" } + * @generates format:message[] messages + */ + function get_messages($user_id) { + $messages = []; + for ($i = 0; $i < 5; $i++) { + $messages[] = [ + "id" => $i, + "name" => "John Doe", + "message" => "hello $i", + "date" => "todo", + ]; + } + return $messages; + } + + function get_last_message($user_id) { + return [ + "id" => 5, + "name" => "John Doe", + "message" => "hello 5", + "date" => "todo", + ]; + } + + ///TODO: validators should not exist, validation should be automatic (or should they? what about specific domain rules) + // validators should only return a bool and a descriptive error message; whether it is input or output validation should be handled elsewhere (should the output be validated?) + // @format group { ... } // formats should be defined on validators so that they can be easily found, additionally the validators enforce their structure, so it is nice when they are next to each other + function validate_group($group) { } +} From 3cb526ff9e1f8ba0a9d9ae37392760f409bca48e Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 20 Nov 2024 07:53:57 +0100 Subject: [PATCH 002/103] added support for shallow meta views --- app/helpers/Swagger/AnnotationHelper.php | 96 ++++++++---- app/model/view/MetaView.php | 180 +++++++++-------------- 2 files changed, 133 insertions(+), 143 deletions(-) diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index 9a01b17f8..efd803c05 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -379,8 +379,7 @@ private static function extractNetteAnnotationParams(array $annotations): array return $bodyParams; } - private static function getMethodAnnotations(string $className, string $methodName): array { - $annotations = self::getMethod($className, $methodName)->getDocComment(); + private static function getAnnotationLines(string $annotations) { $lines = preg_split("/\r\n|\n|\r/", $annotations); # trims whitespace and asterisks @@ -415,6 +414,11 @@ private static function getMethodAnnotations(string $className, string $methodNa return $merged; } + private static function getMethodAnnotations(string $className, string $methodName): array { + $annotations = self::getMethod($className, $methodName)->getDocComment(); + return self::getAnnotationLines($annotations); + } + private static function getRoutePathParamNames(string $route): array { # sample: from '/users/{id}/{name}' generates ['id', 'name'] preg_match_all('/\{([A-Za-z0-9 ]+?)\}/', $route, $out); @@ -459,47 +463,38 @@ private static function filterAnnotations(array $annotations, string $type) { return $rows; } - private static function extractFormatData(array $annotations): array { + private static function extractFormatData(array $annotations) { $formats = []; - $filtered = self::filterAnnotations($annotations, "@format_def"); - foreach ($filtered as $annotation) { - # sample: @format user_info { "name":"format:name", "points":"format:int", "comments":"format:string[]" } - $tokens = explode(" ", $annotation); - $name = $tokens[1]; - - $jsonStart = strpos($annotation, "{"); - $json = substr($annotation, $jsonStart); - $format = json_decode($json); - - $formats[$name] = $format; + $filtered = self::filterAnnotations($annotations, "@format"); + // there should either be one or none format declaration + if (count($filtered) == 0) { + return null; } - return $formats; - } - - private static function extractMethodFormats(string $className, string $methodName): array { - $annotations = self::getMethodAnnotations($className, $methodName); - return self::extractFormatData($annotations); - } - - public static function extractClassFormats(string $className): array { - $methods = get_class_methods($className); - $formatDicts = []; - foreach ($methods as $method) { - $formatDicts[] = self::extractMethodFormats($className, $method); + if (count($filtered) > 1) { + ///TODO: throw exception + echo "Error in extractFormatData: Multiple format definitions.\n"; + return null; } - return array_merge(...$formatDicts); + # sample: @format uuid + $annotation = $filtered[0]; + $tokens = explode(" ", $annotation); + $format = $tokens[1]; + + return $format; } public static function extractMethodCheckedParams(string $className, string $methodName): array { $annotations = self::getMethodAnnotations($className, $methodName); $filtered = self::filterAnnotations($annotations, "@checked_param"); + $formatPrefix = "format:"; + $paramMap = []; foreach ($filtered as $annotation) { // sample: @checked_param format:group group $tokens = explode(" ", $annotation); - $format = $tokens[1]; + $format = substr($tokens[1], strlen($formatPrefix)); $name = $tokens[2]; $paramMap[$name] = $format; } @@ -507,13 +502,50 @@ public static function extractMethodCheckedParams(string $className, string $met return $paramMap; } - public static function extractClassFormat(string $className) { + /** + * Parses the field annotations of a class and returns their metadata. + * @param string $className The name of the class. + * @return array{format: string|null, type: string|null} with the field name as the key. + */ + public static function getClassFormats(string $className) { $class = new \ReflectionClass($className); $fields = get_class_vars($className); + $formats = []; foreach ($fields as $fieldName=>$value) { $field = $class->getProperty($fieldName); - $fieldType = $field->getType()->getName(); - var_dump($fieldType); + $format = self::extractFormatData(self::getAnnotationLines($field->getDocComment())); + # get null if there is no type + $fieldType = $field->getType()?->getName(); + + $formats[$fieldName] = [ + "type" => $fieldType, + "format" => $format, + ]; } + + return $formats; + } + + public static function getFormatDefinitions() { + ///TODO: this should be more sophisticated + $classes = get_declared_classes(); + + // maps format names to class names + $formatClassMap = []; + + foreach ($classes as $className) { + $class = new \ReflectionClass($className); + $annotations = self::getAnnotationLines($class->getDocComment()); + $type_defs = self::filterAnnotations($annotations, "@format_def"); + if (count($type_defs) !== 1) + continue; + + $tokens = explode(" ", $type_defs[0]); + + // the second token is the group name, the first one is the tag + $formatClassMap[$tokens[1]] = $className; + } + + return $formatClassMap; } } diff --git a/app/model/view/MetaView.php b/app/model/view/MetaView.php index 4e6041bc7..e7b6bbec5 100644 --- a/app/model/view/MetaView.php +++ b/app/model/view/MetaView.php @@ -1,64 +1,62 @@ targetTypeInstance, where the instances are filled with the data from the parameters. + */ + function getTypedParams() { + // extract function params of the caller $backtrace = debug_backtrace()[1]; $className = $backtrace['class']; $methodName = $backtrace['function']; - $params_to_values = $this->get_param_names_to_values_map($backtrace); - $params_to_format = AnnotationHelper::extractMethodCheckedParams($className, $methodName); - - foreach ($params_to_values as $param=>$value) { - $format = $params_to_format[$param]; - if (!$this->conforms_to_format($format, $value)) { - ///TODO: debug only - echo "Invalid param <$className:$methodName:$param> value <$value>, given format <$format> \n"; + // get param values + $paramsToValues = $this->getParamNamesToValuesMap($backtrace); + // get param format + $paramsToFormat = AnnotationHelper::extractMethodCheckedParams($className, $methodName); + + // get all format definitions + $formats = AnnotationHelper::getFormatDefinitions(); + + $paramToTypedMap = []; + foreach ($paramsToValues as $paramName=>$paramValue) { + $format = $paramsToFormat[$paramName]; + + if (!array_key_exists($paramName, $paramsToFormat)) { + ///TODO: return 500 + echo "Error: unknown param format: $paramName\n"; + return []; } - } - } - /// extracts function params and returns object representations of the formats - function validate_args() { - $backtrace = debug_backtrace()[1]; - $className = $backtrace['class']; - $methodName = $backtrace['function']; - $params_to_values = $this->get_param_names_to_values_map($backtrace); - $params_to_format = AnnotationHelper::extractMethodCheckedParams($className, $methodName); + $targetClassName = $formats[$format]; + $classFormat = AnnotationHelper::getClassFormats($targetClassName); + $obj = new $targetClassName(); - $a = new GroupFormat(); - AnnotationHelper::extractClassFormat(get_class($a)); + // fill the new object with the param values + ///TODO: handle nested formated objects + foreach ($paramValue as $key=>$value) { + ///TODO: return 404 + if (!array_key_exists($key, $classFormat)) { + echo "Error: unknown param: $paramName\n"; + return []; + } - foreach ($params_to_values as $param=>$value) { - $format = $params_to_format[$param]; - if (!$this->conforms_to_format($format, $value)) { - ///TODO: debug only - echo "Invalid param <$className:$methodName:$param> value <$value>, given format <$format> \n"; + $obj->$key = $value; } + + $paramToTypedMap[$paramName] = $obj; } + + return $paramToTypedMap; } - function get_param_names_to_values_map($backtrace) { + function getParamNamesToValuesMap($backtrace): array { $className = $backtrace['class']; $args = $backtrace['args']; $methodName = $backtrace['function']; @@ -75,7 +73,31 @@ function get_param_names_to_values_map($backtrace) { } } -class GroupFormat { + +class MetaFormat { + /** + * Validates the given format. + * @return bool Returns whether the format and all nested formats are valid. + */ + public function validate() { + return true; + } + + /** + * Validates this format. Automatically called by the validate method on all fields. + * Primitive formats should always override this, composite formats might want to override + * this in case more complex contracts need to be enforced. + * @return bool Returns whether the format is valid. + */ + protected function validate_this() { + + } +} + +/** + * @format_def group + */ +class GroupFormat extends MetaFormat { /** * @format uuid */ @@ -84,26 +106,10 @@ class GroupFormat { * @format uuid */ public string $externalId; - /** - * @format bool - * ///REDUNDANT - */ public bool $organizational; - /** - * @format bool - */ public bool $exam; - /** - * @format bool - */ public bool $archived; - /** - * @format bool - */ public bool $public; - /** - * @format bool - */ public bool $directlyArchived; /** * @format localizedText[] @@ -136,62 +142,14 @@ class GroupFormat { } class TestView extends MetaView { - //function endpoint() { generator(["user_info" /* this would generate the whole user_info object */, "messages":{"name", "message"} /* cherry picking */]) } - - - /// should formats be defined in comments, or in classes? - /// classes: enables autocomplete, enforces structure, creates a ton of data classes, classes are needed for all nested objects - /// comments: no class cluttering, less verbose, no autocomplete, does not enforce structure -> views are created as dictionaries - /** - * @format_def group { - * "id":"format:uuid", - * "externalId":"format:uuid", - * "organizational":"format:bool", - * "exam":"format:bool", - * "archived":"format:bool", - * "public":"format:bool", - * "directlyArchived":"format:bool", - * "localizedTexts":"format:localizedText[]", - * "primaryAdminsIds":"format:uuid[]", - * "parentGroupId":"format:uuid?", - * "parentGroupsIds":"format:uuid[]", - * "childGroups":"format:uuid[]", - * "privateData":"format:groupPrivateData", - * "permissionHints":"format:acl[]" - * } - */ - private $placeholder; - - // here the generator takes an input argument that conforms to format:user_info, therefore the generator can extract named parameters out - // of it and pass them to the database methods - ///TODO: check whether the parameters can support the below annotations with the current framework - /** - * Summary of endpoint - * @format_def user_info { "name":"format:name", "points":"format:int", "comments":"format:string[]" } - * @checked_param format:user_info user_info - * @checked_param format:uuid user_id - * @checked_param format:bool verbose - */ - private $old; - - - /** * @checked_param format:group group * @checked_param format:uuid user_id - * @checked_param format:bool verbose */ - function endpoint($group, $user_id, $verbose) { - $this->validate_args(); - - /*$data = [ - "id" => $user_id, - ]; - $this->generator($data, output_format: "format:group");*/ - - - //$message = $this->get_last_message($user_id); - //$this->generator(output_format: "format:messages[]:text"); + function endpoint($group) { + $params = $this->getTypedParams(); + $formattedGroup = $params["group"]; + var_dump($formattedGroup); } // the names of the format and the output do not have to be identical, the strings in the desired data format refer the output names From 3c6bf16bb6b453b5988ed96e8bcdf2532df511b0 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 5 Dec 2024 12:00:49 +0100 Subject: [PATCH 003/103] added validator logic for meta views WIP --- app/helpers/Swagger/AnnotationHelper.php | 42 +++++++ .../Swagger/PrimitiveFormatValidators.php | 27 +++++ app/model/view/MetaView.php | 112 ++++++++++++++++-- 3 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 app/helpers/Swagger/PrimitiveFormatValidators.php diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index efd803c05..c2de29cd6 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Doctrine\Common\Annotations\AnnotationReader; +use \PrimitiveFormatValidators; use DateTime; @@ -484,6 +485,12 @@ private static function extractFormatData(array $annotations) { return $format; } + /** + * Checks all @checked_param annotations of a method and returns a map from parameter names to their formats. + * @param string $className The name of the containing class. + * @param string $methodName The name of the method. + * @return array + */ public static function extractMethodCheckedParams(string $className, string $methodName): array { $annotations = self::getMethodAnnotations($className, $methodName); $filtered = self::filterAnnotations($annotations, "@checked_param"); @@ -526,6 +533,9 @@ public static function getClassFormats(string $className) { return $formats; } + /** + * Creates a mapping from formats to class names, where the class defines the format. + */ public static function getFormatDefinitions() { ///TODO: this should be more sophisticated $classes = get_declared_classes(); @@ -548,4 +558,36 @@ public static function getFormatDefinitions() { return $formatClassMap; } + + /** + * Extracts all primitive validator methods (starting with "validate") and returns a map from format to a callback. + * The callbacks have one parameter that is passed to the validator. + */ + private static function getPrimitiveValidators(): array { + $instance = new PrimitiveFormatValidators(); + $className = get_class($instance); + $methodNames = get_class_methods($className); + + $validators = []; + foreach ($methodNames as $methodName) { + // all validation methods start with validate + if (!str_starts_with($methodName, "validate")) + continue; + + $annotations = self::getMethodAnnotations($className, $methodName); + $format = self::extractFormatData($annotations); + $callback = function($param) use ($instance, $methodName) { return $instance->$methodName($param); }; + $validators[$format] = $callback; + } + + return $validators; + } + + private static function getMetaValidators(): array { + return []; + } + + private static function getValidators(): array { + return array_merge(self::getPrimitiveValidators(), self::getMetaValidators()); + } } diff --git a/app/helpers/Swagger/PrimitiveFormatValidators.php b/app/helpers/Swagger/PrimitiveFormatValidators.php new file mode 100644 index 000000000..6911f0c09 --- /dev/null +++ b/app/helpers/Swagger/PrimitiveFormatValidators.php @@ -0,0 +1,27 @@ +value; + } +} + diff --git a/app/model/view/MetaView.php b/app/model/view/MetaView.php index e7b6bbec5..aecd57bd1 100644 --- a/app/model/view/MetaView.php +++ b/app/model/view/MetaView.php @@ -28,6 +28,7 @@ function getTypedParams() { foreach ($paramsToValues as $paramName=>$paramValue) { $format = $paramsToFormat[$paramName]; + // the parameter name was not present in the annotations if (!array_key_exists($paramName, $paramsToFormat)) { ///TODO: return 500 echo "Error: unknown param format: $paramName\n"; @@ -40,14 +41,15 @@ function getTypedParams() { // fill the new object with the param values ///TODO: handle nested formated objects - foreach ($paramValue as $key=>$value) { + foreach ($paramValue as $propertyName=>$propertyValue) { ///TODO: return 404 - if (!array_key_exists($key, $classFormat)) { + // the property was not present in the class definition + if (!array_key_exists($propertyName, $classFormat)) { echo "Error: unknown param: $paramName\n"; return []; } - $obj->$key = $value; + $obj->$propertyName = $propertyValue; } $paramToTypedMap[$paramName] = $obj; @@ -73,24 +75,117 @@ function getParamNamesToValuesMap($backtrace): array { } } +/** + * Parses format string enriched by nullability and array modifiers. + * In case the format contains array, this data class can be recursive. + * Example: string?[]? can either be null or of string?[] type, an array of nullable strings + * Example2: string[]?[] is an array of null or string arrays + */ +class FormatParser { + public bool $nullable = false; + public bool $isArray = false; + // contains the format stripped of the nullability ?, null if it is an array + public ?string $format = null; + // contains the format definition of nested elements, null if it is not an array + public ?FormatParser $nested = null; + + public function __construct(string $format) { + // check nullability + if (str_ends_with($format, "?")) { + $this->nullable = true; + $format = substr($format, 0, -1); + } + + // check array + if (str_ends_with($format, "[]")) { + $this->isArray = true; + $format = substr($format, 0, -2); + $this->nested = new FormatParser($format); + } + else { + $this->format = $format; + } + } +} + class MetaFormat { + // validates primitive formats of intrinsic PHP types + ///TODO: make this static somehow (or cached) + private $validators; + + public function __construct() { + $this->validators = AnnotationHelper::getValidators(); + } + + /** * Validates the given format. * @return bool Returns whether the format and all nested formats are valid. */ public function validate() { - return true; + // check whether all higher level contracts hold + if (!$this->validateSelf()) + return false; + + // check properties + $selfFormat = AnnotationHelper::getClassFormats(get_class($this)); + foreach ($selfFormat as $propertyName=>$propertyFormat) { + ///TODO: check if this is true + /// if the property is checked by type only, there is no need to check it as an invalid assignment would rise an error + $value = $this->$propertyName; + $format = $propertyFormat["format"]; + if ($format === null) + continue; + + // enables parsing more complicated formats (string[]?, string?[], string?[][]?, ...) + $parsedFormat = new FormatParser($format); + if (!$this->recursiveFormatChecker($value, $parsedFormat)) + return false; + } + + } + + private function recursiveFormatChecker($value, FormatParser $parsedFormat) { + // enables parsing more complicated formats (string[]?, string?[], string?[][]?, ...) + + // check nullability + if ($value === null) + return $parsedFormat->nullable; + + // handle arrays + if ($parsedFormat->isArray) { + if (!is_array($value)) + return false; + + // if any element fails, the whole format fails + foreach ($value as $element) { + if (!$this->recursiveFormatChecker($element, $parsedFormat->nested)) + return false; + } + return true; + } + + ///TODO: raise an error + // check whether the validator exists + if (!array_key_exists($parsedFormat->format, $this->validators)) { + echo "Error: missing validator for format: " . $parsedFormat->format . "\n"; + return false; + } + + return $this->validators[$parsedFormat->format]($value); } /** * Validates this format. Automatically called by the validate method on all fields. * Primitive formats should always override this, composite formats might want to override * this in case more complex contracts need to be enforced. + * This method should not check the format of nested types. * @return bool Returns whether the format is valid. */ - protected function validate_this() { - + protected function validateSelf() { + // there are no constraints by default + return true; } } @@ -146,10 +241,13 @@ class TestView extends MetaView { * @checked_param format:group group * @checked_param format:uuid user_id */ - function endpoint($group) { + function endpoint($group, $user_id) { $params = $this->getTypedParams(); $formattedGroup = $params["group"]; var_dump($formattedGroup); + + // $a = new GroupFormat(); + // $a->validate(); } // the names of the format and the output do not have to be identical, the strings in the desired data format refer the output names From a0459bc8960bc973948c52e50c5507613255b268 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 11 Dec 2024 20:15:43 +0100 Subject: [PATCH 004/103] added a mechanism that can read format definitions from a folder --- app/helpers/MetaFormats/MetaFormatHelper.php | 15 +++++++++++++-- app/model/view/MetaView.php | 13 +++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 1014824cf..b8da1dba1 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -7,6 +7,9 @@ class MetaFormatHelper { + private static string $formatDefinitionFolder = __DIR__ . '/FormatDefinitions'; + private static string $formatDefinitionsNamespace = "App\\Helpers\\MetaFormats\\FormatDefinitions"; + private static function extractFormatData(array $annotations) { $filtered = AnnotationHelper::filterAnnotations($annotations, "@format"); @@ -83,8 +86,16 @@ public static function getClassFormats(string $className) */ public static function getFormatDefinitions() { - ///TODO: this should be more sophisticated - $classes = get_declared_classes(); + // scan directory of format definitions + $formatFiles = scandir(self::$formatDefinitionFolder); + // filter out only format files ending with 'Format.php' + $formatFiles = array_filter($formatFiles, function ($file) { + return str_ends_with($file, "Format.php"); + }); + $classes = array_map(function (string $file) { + $fileWithoutExtension = substr($file, 0, -4); + return self::$formatDefinitionsNamespace . "\\$fileWithoutExtension"; + }, $formatFiles); // maps format names to class names $formatClassMap = []; diff --git a/app/model/view/MetaView.php b/app/model/view/MetaView.php index 61ebf0c36..81c1c74e1 100644 --- a/app/model/view/MetaView.php +++ b/app/model/view/MetaView.php @@ -4,6 +4,7 @@ use App\Helpers\Swagger\AnnotationHelper; use App\Helpers\MetaFormats\MetaFormatHelper; +use App\Exceptions\InternalServerException; // parent class of all meta classes @@ -31,13 +32,17 @@ public function getTypedParams() $paramToTypedMap = []; foreach ($paramsToValues as $paramName => $paramValue) { - $format = $paramsToFormat[$paramName]; // the parameter name was not present in the annotations if (!array_key_exists($paramName, $paramsToFormat)) { - ///TODO: return 500 - echo "Error: unknown param format: $paramName\n"; - return []; + throw new InternalServerException("Unknown method parameter format: $paramName\n"); + } + + $format = $paramsToFormat[$paramName]; + + // the format is not defined + if (!array_key_exists($format, $formats)) { + throw new InternalServerException("The format does not have a definition class: $format\n"); } $targetClassName = $formats[$format]; From a1adcc6fc9ebd46bf0b23b5a79c9548df258095d Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 13 Dec 2024 17:22:55 +0100 Subject: [PATCH 005/103] replaced annotations with attributes in format classes --- app/helpers/MetaFormats/FormatAttribute.php | 16 ++++++++ .../FormatDefinitions/GroupFormat.php | 41 +++++-------------- app/helpers/MetaFormats/MetaFormatHelper.php | 37 ++++++++++++----- 3 files changed, 54 insertions(+), 40 deletions(-) create mode 100644 app/helpers/MetaFormats/FormatAttribute.php diff --git a/app/helpers/MetaFormats/FormatAttribute.php b/app/helpers/MetaFormats/FormatAttribute.php new file mode 100644 index 000000000..67d11df0d --- /dev/null +++ b/app/helpers/MetaFormats/FormatAttribute.php @@ -0,0 +1,16 @@ +getAttributes(FormatAttribute::class); + $name = $reflectionObject->getName(); + if (count($formatAttributes) === 0) { + return null; + } + + // check attribute correctness + $formatArguments = $formatAttributes[0]->getArguments(); + if (count($formatArguments) !== 1) { + throw new InternalServerException("The entity $name does not have a single attribute argument."); + } + + return $formatArguments[0]; + } + /** - * Parses the field annotations of a class and returns their metadata. + * Parses the format attributes of class fields and returns their metadata. * @param string $className The name of the class. * @return array{format: string|null, type: string|null} with the field name as the key. */ @@ -68,7 +87,8 @@ public static function getClassFormats(string $className) $formats = []; foreach ($fields as $fieldName => $value) { $field = $class->getProperty($fieldName); - $format = self::extractFormatData(AnnotationHelper::getAnnotationLines($field->getDocComment())); + // the format can be null (not present) + $format = self::extractFormatFromAttribute($field); // get null if there is no type $fieldType = $field->getType()?->getName(); @@ -101,17 +121,14 @@ public static function getFormatDefinitions() $formatClassMap = []; foreach ($classes as $className) { + // get the format attribute $class = new ReflectionClass($className); - $annotations = AnnotationHelper::getAnnotationLines($class->getDocComment()); - $type_defs = AnnotationHelper::filterAnnotations($annotations, "@format_def"); - if (count($type_defs) !== 1) { - continue; + $format = self::extractFormatFromAttribute($class); + if ($format === null) { + throw new InternalServerException("The class {$className} does not have the format attribute."); } - $tokens = explode(" ", $type_defs[0]); - - // the second token is the group name, the first one is the tag - $formatClassMap[$tokens[1]] = $className; + $formatClassMap[$format] = $className; } return $formatClassMap; From c98bad258807f181700fa6bcfdb07fb40b3278b0 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Mon, 16 Dec 2024 14:02:56 +0100 Subject: [PATCH 006/103] each class field can now be checked individually, added cache for reflective structures --- app/commands/MetaTester.php | 16 ++- .../MetaFormats/FieldFormatDefinition.php | 104 ++++++++++++++++++ app/helpers/MetaFormats/FormatCache.php | 69 ++++++++++++ app/helpers/MetaFormats/MetaFormat.php | 71 +++--------- app/helpers/MetaFormats/MetaFormatHelper.php | 9 +- .../MetaFormats/PrimitiveFormatValidators.php | 4 +- 6 files changed, 203 insertions(+), 70 deletions(-) create mode 100644 app/helpers/MetaFormats/FieldFormatDefinition.php create mode 100644 app/helpers/MetaFormats/FormatCache.php diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php index 8c5a1cfa3..f7012a2e1 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaTester.php @@ -2,6 +2,7 @@ namespace App\Console; +use App\Helpers\MetaFormats\FormatDefinitions\GroupFormat; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -28,11 +29,14 @@ protected function execute(InputInterface $input, OutputInterface $output) public function test(string $arg) { - $view = new TestView(); - $view->endpoint([ - "id" => "0", - "organizational" => false, - ], "0001"); - // $view->get_user_info(0); + // $view = new TestView(); + // $view->endpoint([ + // "id" => "0", + // "organizational" => false, + // ], "0001"); + // // $view->get_user_info(0); + + $format = new GroupFormat(); + var_dump($format->checkIfAssignable("primaryAdminsIds", [ "10000000-2000-4000-8000-160000000000", "10000000-2000-4000-8000-160000000000" ])); } } diff --git a/app/helpers/MetaFormats/FieldFormatDefinition.php b/app/helpers/MetaFormats/FieldFormatDefinition.php new file mode 100644 index 000000000..1e51e56d7 --- /dev/null +++ b/app/helpers/MetaFormats/FieldFormatDefinition.php @@ -0,0 +1,104 @@ + "bool", + "integer" => "int", + "double" => "double", + "string" => "string", + "array" => "array", + "object" => "object", + "resource" => "resource", + "NULL" => "null", + ]; + + /** + * Constructs a field format definition. + * Either the @format or @type parameter need to have a non-null value (or both). + * @param ?string $format The format of the field. + * @param ?string $type The PHP type of the field yielded by a 'ReflectionProperty::getType()' call. + * @throws \App\Exceptions\InternalServerException Thrown when both @format and @type were null. + */ + public function __construct(?string $format, ?string $type) + { + // if both are null, there is no way to validate an assigned value + if ($format === null && $type === null) { + throw new InternalServerException("Both the format and type of a field definition were undefined."); + } + + $this->format = $format; + $this->type = $type; + } + + /** + * Checks whether a value meets this definition. + * @param mixed $value The value to be checked. + * @throws \App\Exceptions\InternalServerException Thrown when the format does not have a validator. + * @return bool Returns whether the value passed the test. + */ + public function conformsToDefinition(mixed $value) + { + // use format validators if possible + if ($this->format !== null) { + // enables parsing more complicated formats (string[]?, string?[], string?[][]?, ...) + $parsedFormat = new FormatParser($this->format); + return self::recursiveFormatChecker($value, $parsedFormat); + } + + // convert the gettype return value to the reflective return value + $valueType = gettype($value); + if (!array_key_exists($valueType, self::$gettypeToReflectiveMap)) { + throw new InternalServerException("Unknown gettype value: $valueType"); + } + return $valueType === $this->type; + } + + /** + * Checks whether the value fits a format recursively. + * The format can contain array modifiers and thus all array elements need to be checked recursively. + * @param mixed $value The value to be checked + * @param \App\Helpers\MetaFormats\FormatParser $parsedFormat A parsed format used for recursive traversal. + * @throws \App\Exceptions\InternalServerException Thrown when a format does not have a validator. + * @return bool Returns whether the value conforms to the format. + */ + private static function recursiveFormatChecker(mixed $value, FormatParser $parsedFormat): bool + { + // check nullability + if ($value === null) { + return $parsedFormat->nullable; + } + + // handle arrays + if ($parsedFormat->isArray) { + if (!is_array($value)) { + return false; + } + + // if any element fails, the whole format fails + foreach ($value as $element) { + if (!self::recursiveFormatChecker($element, $parsedFormat->nested)) { + return false; + } + } + return true; + } + + // check whether the validator exists + $validators = FormatCache::getValidators(); + if (!array_key_exists($parsedFormat->format, $validators)) { + throw new InternalServerException("The format {$parsedFormat->format} does not have a validator."); + } + + return $validators[$parsedFormat->format]($value); + } +} diff --git a/app/helpers/MetaFormats/FormatCache.php b/app/helpers/MetaFormats/FormatCache.php new file mode 100644 index 000000000..0426703c6 --- /dev/null +++ b/app/helpers/MetaFormats/FormatCache.php @@ -0,0 +1,69 @@ + $class) { + self::$classToFormatMap[$class] = $format; + } + } + return self::$classToFormatMap; + } + + public static function getFormatToFieldDefinitionsMap(): array + { + if (self::$formatToFieldFormatsMap == null) { + self::$formatToFieldFormatsMap = []; + $formatToClassMap = self::getFormatToClassMap(); + foreach ($formatToClassMap as $format => $class) { + self::$formatToFieldFormatsMap[$format] = MetaFormatHelper::createNameToFieldDefinitionsMap($class); + } + } + return self::$formatToFieldFormatsMap; + } + + public static function getValidators(): array + { + if (self::$validators == null) { + self::$validators = MetaFormatHelper::getValidators(); + } + return self::$validators; + } + + public static function getFieldDefinitions(string $className) + { + $classToFormatMap = self::getClassToFormatMap(); + if (!array_key_exists($className, $classToFormatMap)) { + throw new InternalServerException("The class $className does not have a format definition."); + } + + $format = $classToFormatMap[$className]; + $formatToFieldFormatsMap = self::getFormatToFieldDefinitionsMap(); + if (!array_key_exists($format, $formatToFieldFormatsMap)) { + throw new InternalServerException("The format $format does not have a field format definition."); + } + + return $formatToFieldFormatsMap[$format]; + } +} diff --git a/app/helpers/MetaFormats/MetaFormat.php b/app/helpers/MetaFormats/MetaFormat.php index bb5b4944f..8579dd81e 100644 --- a/app/helpers/MetaFormats/MetaFormat.php +++ b/app/helpers/MetaFormats/MetaFormat.php @@ -2,20 +2,24 @@ namespace App\Helpers\MetaFormats; +use App\Exceptions\InternalServerException; use App\Helpers\Swagger\AnnotationHelper; +use function Symfony\Component\String\b; + class MetaFormat { - // validates primitive formats of intrinsic PHP types - ///TODO: make this static somehow (or cached) - private $validators; - - public function __construct() + public function checkIfAssignable(string $fieldName, mixed $value): bool { - $this->validators = MetaFormatHelper::getValidators(); + $fieldFormats = FormatCache::getFieldDefinitions(get_class($this)); + if (!array_key_exists($fieldName, $fieldFormats)) { + throw new InternalServerException("The field name $fieldName is not present in the format definition."); + } + // get the definition for the specific field + $formatDefinition = $fieldFormats[$fieldName]; + return $formatDefinition->conformsToDefinition($value); } - /** * Validates the given format. * @return bool Returns whether the format and all nested formats are valid. @@ -27,21 +31,10 @@ public function validate() return false; } - // check properties - $selfFormat = MetaFormatHelper::getClassFormats(get_class($this)); - foreach ($selfFormat as $propertyName => $propertyFormat) { - ///TODO: check if this is true - /// if the property is checked by type only, there is no need to check it as an invalid assignment - /// would rise an error - $value = $this->$propertyName; - $format = $propertyFormat["format"]; - if ($format === null) { - continue; - } - - // enables parsing more complicated formats (string[]?, string?[], string?[][]?, ...) - $parsedFormat = new FormatParser($format); - if (!$this->recursiveFormatChecker($value, $parsedFormat)) { + // go through all fields and check whether they were assigned properly + $fieldFormats = FormatCache::getFieldDefinitions(get_class($this)); + foreach ($fieldFormats as $fieldName => $fieldFormat) { + if (!$this->checkIfAssignable($fieldName, $this->$fieldName)) { return false; } } @@ -49,40 +42,6 @@ public function validate() return true; } - private function recursiveFormatChecker($value, FormatParser $parsedFormat) - { - // enables parsing more complicated formats (string[]?, string?[], string?[][]?, ...) - - // check nullability - if ($value === null) { - return $parsedFormat->nullable; - } - - // handle arrays - if ($parsedFormat->isArray) { - if (!is_array($value)) { - return false; - } - - // if any element fails, the whole format fails - foreach ($value as $element) { - if (!$this->recursiveFormatChecker($element, $parsedFormat->nested)) { - return false; - } - } - return true; - } - - ///TODO: raise an error - // check whether the validator exists - if (!array_key_exists($parsedFormat->format, $this->validators)) { - echo "Error: missing validator for format: " . $parsedFormat->format . "\n"; - return false; - } - - return $this->validators[$parsedFormat->format]($value); - } - /** * Validates this format. Automatically called by the validate method on all fields. * Primitive formats should always override this, composite formats might want to override diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 92cadcced..c95da5238 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -80,7 +80,7 @@ private static function extractFormatFromAttribute(ReflectionClass|ReflectionPro * @param string $className The name of the class. * @return array{format: string|null, type: string|null} with the field name as the key. */ - public static function getClassFormats(string $className) + public static function createNameToFieldDefinitionsMap(string $className) { $class = new ReflectionClass($className); $fields = get_class_vars($className); @@ -92,10 +92,7 @@ public static function getClassFormats(string $className) // get null if there is no type $fieldType = $field->getType()?->getName(); - $formats[$fieldName] = [ - "type" => $fieldType, - "format" => $format, - ]; + $formats[$fieldName] = new FieldFormatDefinition($format, $fieldType); } return $formats; @@ -104,7 +101,7 @@ public static function getClassFormats(string $className) /** * Creates a mapping from formats to class names, where the class defines the format. */ - public static function getFormatDefinitions() + public static function createFormatToClassMap() { // scan directory of format definitions $formatFiles = scandir(self::$formatDefinitionFolder); diff --git a/app/helpers/MetaFormats/PrimitiveFormatValidators.php b/app/helpers/MetaFormats/PrimitiveFormatValidators.php index 339e071b4..c5076a1b0 100644 --- a/app/helpers/MetaFormats/PrimitiveFormatValidators.php +++ b/app/helpers/MetaFormats/PrimitiveFormatValidators.php @@ -7,7 +7,7 @@ class PrimitiveFormatValidators /** * @format uuid */ - public function validateUuid($uuid) + public function validateUuid($uuid): bool { if (!self::checkType($uuid, PhpTypes::String)) { return false; @@ -16,7 +16,7 @@ public function validateUuid($uuid) return preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $uuid) === 1; } - private static function checkType($value, PhpTypes $type) + private static function checkType($value, PhpTypes $type): bool { return gettype($value) === $type->value; } From bb5f8ee6f08f1c22a1fa1ff1fbbc2e18668aad4f Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Tue, 17 Dec 2024 17:51:20 +0100 Subject: [PATCH 007/103] added user format, changed the registration endpoint to accept a request wrapper returning a format instance --- .../presenters/RegistrationPresenter.php | 5 +- .../presenters/base/BasePresenter.php | 93 ++++++++++++------ app/commands/MetaTester.php | 8 +- .../MetaFormats/FieldFormatDefinition.php | 11 ++- app/helpers/MetaFormats/FormatCache.php | 9 ++ .../FormatDefinitions/UserFormat.php | 20 ++++ app/helpers/MetaFormats/MetaFormat.php | 23 +++++ app/helpers/MetaFormats/MetaFormatHelper.php | 39 +++++++- app/helpers/MetaFormats/MetaRequest.php | 94 +++++++++++++++++++ 9 files changed, 265 insertions(+), 37 deletions(-) create mode 100644 app/helpers/MetaFormats/FormatDefinitions/UserFormat.php create mode 100644 app/helpers/MetaFormats/MetaRequest.php diff --git a/app/V1Module/presenters/RegistrationPresenter.php b/app/V1Module/presenters/RegistrationPresenter.php index a8753a3b3..4a2564fc2 100644 --- a/app/V1Module/presenters/RegistrationPresenter.php +++ b/app/V1Module/presenters/RegistrationPresenter.php @@ -21,11 +21,13 @@ use App\Helpers\EmailVerificationHelper; use App\Helpers\RegistrationConfig; use App\Helpers\InvitationHelper; +use App\Helpers\MetaFormats\FormatAttribute; use App\Security\Roles; use App\Security\ACL\IUserPermissions; use App\Security\ACL\IGroupPermissions; use Nette\Http\IResponse; use Nette\Security\Passwords; +use Tracy\ILogger; use ZxcvbnPhp\Zxcvbn; /** @@ -161,9 +163,10 @@ public function checkCreateAccount() * @throws WrongCredentialsException * @throws InvalidArgumentException */ + #[FormatAttribute("userRegistration")] public function actionCreateAccount() { - $req = $this->getRequest(); + $req = $this->getMetaRequest(); // check if the email is free $email = trim($req->getPost("email")); diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 383a64044..43ea6a202 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -2,6 +2,7 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\MetaFormatHelper; use App\Helpers\Pagination; use App\Model\Entity\User; use App\Security\AccessToken; @@ -21,6 +22,9 @@ use App\Helpers\Validators; use App\Helpers\FileStorage\IImmutableFile; use App\Helpers\AnnotationsParser; +use App\Helpers\MetaFormats\FormatCache; +use App\Helpers\MetaFormats\MetaFormat; +use App\Helpers\MetaFormats\MetaRequest; use App\Responses\StorageFileResponse; use App\Responses\ZipFilesResponse; use Nette\Application\Application; @@ -70,8 +74,8 @@ class BasePresenter extends \App\Presenters\BasePresenter */ public $logger; - /** @var object Processed parameters from annotations */ - protected $parameters; + /** @var MetaFormat Processed parameters from the request */ + private MetaFormat $requestFormatInstance; protected function formatPermissionCheckMethod($action) { @@ -106,7 +110,6 @@ public function startup() { parent::startup(); $this->application->errorPresenter = "V1:ApiError"; - $this->parameters = new \stdClass(); try { $presenterReflection = new ReflectionClass($this); @@ -130,6 +133,8 @@ public function startup() Validators::init(); $this->processParams($actionReflection); + + $this->logger->log(var_export($this->getRequest(), true), ILogger::DEBUG); } protected function isRequestJson(): bool @@ -193,36 +198,68 @@ protected function isInScope(string $scope): bool return $identity->isInScope($scope); } + public function getMetaRequest(): MetaRequest|null + { + $request = parent::getRequest(); + return new MetaRequest($request, $this->requestFormatInstance); + } + private function processParams(ReflectionMethod $reflection) { - $annotations = AnnotationsParser::getAll($reflection); - $requiredFields = Arrays::get($annotations, "Param", []); - - foreach ($requiredFields as $field) { - $type = strtolower($field->type); - $name = $field->name; - $validationRule = isset($field->validation) ? $field->validation : null; - $msg = isset($field->msg) ? $field->msg : null; - $required = isset($field->required) ? $field->required : true; - - $value = null; - switch ($type) { - case "post": - $value = $this->getPostField($name, $required); - break; - case "query": - $value = $this->getQueryField($name, $required); - break; - default: - throw new InternalServerException("Unknown parameter type '$type'"); - } + // $annotations = AnnotationsParser::getAll($reflection); + // $requiredFields = Arrays::get($annotations, "Param", []); - if ($validationRule !== null && $value !== null) { - $value = $this->validateValue($name, $value, $validationRule, $msg); - } + ///TODO: add support for post/query type distinction + $format = MetaFormatHelper::extractFormatFromAttribute($reflection); - $this->parameters->$name = $value; + // ignore request that do not yet have the attribute + if ($format === null) { + return; } + + $fieldNames = FormatCache::getFormatFieldNames($format); + $formatInstance = MetaFormatHelper::createFormatInstance($format); + foreach ($fieldNames as $field) { + ///TODO: check if required + $value = $this->getPostField($field, false); + $this->logger->log(var_export([$field, $value], true), ILogger::DEBUG); + if (!$formatInstance->checkedAssign($field, $value)) { + ///TODO: it would be nice to give a more detailed error message here + throw new InvalidArgumentException($field); + } + } + + $this->requestFormatInstance = $formatInstance; + $this->logger->log(var_export($formatInstance, true), ILogger::DEBUG); + + // $this->logger->log(var_export($annotations, true), ILogger::DEBUG); + // $this->logger->log(var_export($requiredFields, true), ILogger::DEBUG); + + // foreach ($requiredFields as $field) { + // $type = strtolower($field->type); + // $name = $field->name; + // $validationRule = isset($field->validation) ? $field->validation : null; + // $msg = isset($field->msg) ? $field->msg : null; + // $required = isset($field->required) ? $field->required : true; + + // $this->logger->log("test", ILogger::DEBUG); + + // $value = null; + // switch ($type) { + // case "post": + // $value = $this->getPostField($name, $required); + // break; + // case "query": + // $value = $this->getQueryField($name, $required); + // break; + // default: + // throw new InternalServerException("Unknown parameter type '$type'"); + // } + + // if ($validationRule !== null && $value !== null) { + // $value = $this->validateValue($name, $value, $validationRule, $msg); + // } + // } } private function getPostField($param, $required = true) diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php index f7012a2e1..5f767200a 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaTester.php @@ -3,6 +3,7 @@ namespace App\Console; use App\Helpers\MetaFormats\FormatDefinitions\GroupFormat; +use App\Helpers\MetaFormats\FormatDefinitions\UserFormat; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -36,7 +37,10 @@ public function test(string $arg) // ], "0001"); // // $view->get_user_info(0); - $format = new GroupFormat(); - var_dump($format->checkIfAssignable("primaryAdminsIds", [ "10000000-2000-4000-8000-160000000000", "10000000-2000-4000-8000-160000000000" ])); + // $format = new GroupFormat(); + // var_dump($format->checkIfAssignable("primaryAdminsIds", [ "10000000-2000-4000-8000-160000000000", "10000000-2000-4000-8000-160000000000" ])); + + $format = new UserFormat(); + $format->checkedAssign("titlesBeforeName", null); } } diff --git a/app/helpers/MetaFormats/FieldFormatDefinition.php b/app/helpers/MetaFormats/FieldFormatDefinition.php index 1e51e56d7..1ee7f0e44 100644 --- a/app/helpers/MetaFormats/FieldFormatDefinition.php +++ b/app/helpers/MetaFormats/FieldFormatDefinition.php @@ -9,6 +9,7 @@ class FieldFormatDefinition public ?string $format; // A string name of the field type yielded by 'ReflectionProperty::getType()'. public ?string $type; + public bool $nullable; ///TODO: double check this private static array $gettypeToReflectiveMap = [ @@ -27,9 +28,10 @@ class FieldFormatDefinition * Either the @format or @type parameter need to have a non-null value (or both). * @param ?string $format The format of the field. * @param ?string $type The PHP type of the field yielded by a 'ReflectionProperty::getType()' call. + * @param bool $nullable Whether the type is nullable. * @throws \App\Exceptions\InternalServerException Thrown when both @format and @type were null. */ - public function __construct(?string $format, ?string $type) + public function __construct(?string $format, ?string $type, bool $nullable) { // if both are null, there is no way to validate an assigned value if ($format === null && $type === null) { @@ -38,6 +40,7 @@ public function __construct(?string $format, ?string $type) $this->format = $format; $this->type = $type; + $this->nullable = $nullable; } /** @@ -55,11 +58,17 @@ public function conformsToDefinition(mixed $value) return self::recursiveFormatChecker($value, $parsedFormat); } + // if the value is null and is a base type, check the nullability of the base type + if ($value == null) { + return $this->nullable; + } + // convert the gettype return value to the reflective return value $valueType = gettype($value); if (!array_key_exists($valueType, self::$gettypeToReflectiveMap)) { throw new InternalServerException("Unknown gettype value: $valueType"); } + return $valueType === $this->type; } diff --git a/app/helpers/MetaFormats/FormatCache.php b/app/helpers/MetaFormats/FormatCache.php index 0426703c6..249b65922 100644 --- a/app/helpers/MetaFormats/FormatCache.php +++ b/app/helpers/MetaFormats/FormatCache.php @@ -43,6 +43,15 @@ public static function getFormatToFieldDefinitionsMap(): array return self::$formatToFieldFormatsMap; } + public static function getFormatFieldNames(string $format): array + { + $formatToFieldDefinitionsMap = self::getFormatToFieldDefinitionsMap(); + if (!array_key_exists($format, $formatToFieldDefinitionsMap)) { + throw new InternalServerException("The format $format does not have a field format definition."); + } + return array_keys($formatToFieldDefinitionsMap[$format]); + } + public static function getValidators(): array { if (self::$validators == null) { diff --git a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php new file mode 100644 index 000000000..7ad250f16 --- /dev/null +++ b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php @@ -0,0 +1,20 @@ +conformsToDefinition($value); } + /** + * Tries to assign a value to a field. If the value does not conform to the field format, it will not be assigned. + * @param string $fieldName The name of the field. + * @param mixed $value The value to be assigned. + * @return bool Returns whether the value was assigned. + */ + public function checkedAssign(string $fieldName, mixed $value) + { + if (!$this->checkIfAssignable($fieldName, $value)) { + return false; + } + + $this->$fieldName = $value; + return true; + } + /** * Validates the given format. * @return bool Returns whether the format and all nested formats are valid. diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index c95da5238..a9fa79b6a 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -5,6 +5,7 @@ use App\Exceptions\InternalServerException; use ReflectionClass; use App\Helpers\Swagger\AnnotationHelper; +use ReflectionMethod; use ReflectionProperty; class MetaFormatHelper @@ -58,10 +59,17 @@ public static function extractMethodCheckedParams(string $className, string $met return $paramMap; } - private static function extractFormatFromAttribute(ReflectionClass|ReflectionProperty $reflectionObject): ?string - { + /** + * Checks whether an entity contains a FormatAttribute and extracts the format if so. + * @param \ReflectionClass|\ReflectionProperty|\ReflectionMethod $reflectionObject A reflection + * object of the entity. + * @throws \App\Exceptions\InternalServerException Thrown when the FormatAttribute was used incorrectly. + * @return ?string Returns the format or null if no FormatAttribute was present. + */ + public static function extractFormatFromAttribute( + ReflectionClass|ReflectionProperty|ReflectionMethod $reflectionObject + ): ?string { $formatAttributes = $reflectionObject->getAttributes(FormatAttribute::class); - $name = $reflectionObject->getName(); if (count($formatAttributes) === 0) { return null; } @@ -69,6 +77,7 @@ private static function extractFormatFromAttribute(ReflectionClass|ReflectionPro // check attribute correctness $formatArguments = $formatAttributes[0]->getArguments(); if (count($formatArguments) !== 1) { + $name = $reflectionObject->getName(); throw new InternalServerException("The entity $name does not have a single attribute argument."); } @@ -90,9 +99,11 @@ public static function createNameToFieldDefinitionsMap(string $className) // the format can be null (not present) $format = self::extractFormatFromAttribute($field); // get null if there is no type - $fieldType = $field->getType()?->getName(); + $reflectionType = $field->getType(); + $fieldType = $reflectionType?->getName(); + $nullable = $reflectionType?->allowsNull() ?? false; - $formats[$fieldName] = new FieldFormatDefinition($format, $fieldType); + $formats[$fieldName] = new FieldFormatDefinition($format, $fieldType, $nullable); } return $formats; @@ -168,4 +179,22 @@ public static function getValidators(): array { return array_merge(self::getPrimitiveValidators(), self::getMetaValidators()); } + + /** + * Creates a MetaFormat instance of the given format. + * @param string $format The name of the format. + * @throws \App\Exceptions\InternalServerException Thrown when the format does not exist. + * @return \App\Helpers\MetaFormats\MetaFormat Returns the constructed MetaFormat instance. + */ + public static function createFormatInstance(string $format): MetaFormat + { + $formatToClassMap = FormatCache::getFormatToClassMap(); + if (!array_key_exists($format, $formatToClassMap)) { + throw new InternalServerException("The format $format does not exist."); + } + + $className = $formatToClassMap[$format]; + $instance = new $className(); + return $instance; + } } diff --git a/app/helpers/MetaFormats/MetaRequest.php b/app/helpers/MetaFormats/MetaRequest.php new file mode 100644 index 000000000..af116996b --- /dev/null +++ b/app/helpers/MetaFormats/MetaRequest.php @@ -0,0 +1,94 @@ +baseRequest = $request; + $this->requestFormatInstance = $requestFormatInstance; + } + + /** + * Retrieve the presenter name. + */ + public function getPresenterName(): string + { + return $this->baseRequest->getPresenterName(); + } + + /** + * Returns all variables provided to the presenter (usually via URL). + */ + public function getParameters(): array + { + return $this->baseRequest->getParameters(); + } + + /** + * Returns a parameter provided to the presenter. + */ + public function getParameter(string $key): mixed + { + return $this->baseRequest->getParameter($key); + } + + /** + * Returns a variable provided to the presenter via POST. + * If no key is passed, returns the entire array. + */ + ///TODO: how should null be handled? + public function getPost(?string $key = null): mixed + { + if ($key === null) { + return $this->requestFormatInstance; + } + + return $this->requestFormatInstance->$key; + } + + /** + * Returns all uploaded files. + */ + public function getFiles(): array + { + return $this->baseRequest->getFiles(); + } + + /** + * Returns the method. + */ + public function getMethod(): ?string + { + return $this->baseRequest->getMethod(); + } + + + /** + * Checks if the method is the given one. + */ + public function isMethod(string $method): bool + { + return $this->baseRequest->isMethod($method); + } + + /** + * Checks the flag. + */ + public function hasFlag(string $flag): bool + { + return $this->baseRequest->hasFlag($flag); + } + + + public function toArray(): array + { + return $this->baseRequest->toArray(); + } +} From aaf4ea6dfc3948f3a82ead1d53c9ea393ca910c3 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Tue, 17 Dec 2024 18:11:58 +0100 Subject: [PATCH 008/103] added recursion to validation, made structural validation a public method --- app/V1Module/presenters/base/BasePresenter.php | 8 ++++++-- app/helpers/MetaFormats/MetaFormat.php | 14 +++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 43ea6a202..6443563ed 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -217,20 +217,24 @@ private function processParams(ReflectionMethod $reflection) return; } + ///TODO: handle nested MetaFormat creation $fieldNames = FormatCache::getFormatFieldNames($format); $formatInstance = MetaFormatHelper::createFormatInstance($format); foreach ($fieldNames as $field) { ///TODO: check if required $value = $this->getPostField($field, false); - $this->logger->log(var_export([$field, $value], true), ILogger::DEBUG); if (!$formatInstance->checkedAssign($field, $value)) { ///TODO: it would be nice to give a more detailed error message here throw new InvalidArgumentException($field); } } + // validate structural constraints + if (!$formatInstance->validateStructure()) { + throw new BadRequestException("All request fields are valid but additional structural constraints failed."); + } + $this->requestFormatInstance = $formatInstance; - $this->logger->log(var_export($formatInstance, true), ILogger::DEBUG); // $this->logger->log(var_export($annotations, true), ILogger::DEBUG); // $this->logger->log(var_export($requiredFields, true), ILogger::DEBUG); diff --git a/app/helpers/MetaFormats/MetaFormat.php b/app/helpers/MetaFormats/MetaFormat.php index 9febdaf20..3093643a0 100644 --- a/app/helpers/MetaFormats/MetaFormat.php +++ b/app/helpers/MetaFormats/MetaFormat.php @@ -50,7 +50,7 @@ public function checkedAssign(string $fieldName, mixed $value) public function validate() { // check whether all higher level contracts hold - if (!$this->validateSelf()) { + if (!$this->validateStructure()) { return false; } @@ -60,19 +60,23 @@ public function validate() if (!$this->checkIfAssignable($fieldName, $this->$fieldName)) { return false; } + + // check nested formats recursively + if ($this->$fieldName instanceof MetaFormat && !$this->$fieldName->validate()) { + return false; + } } return true; } /** - * Validates this format. Automatically called by the validate method on all fields. - * Primitive formats should always override this, composite formats might want to override - * this in case more complex contracts need to be enforced. + * Validates this format. Automatically called by the validate method on all suitable fields. + * Formats might want to override this in case more complex contracts need to be enforced. * This method should not check the format of nested types. * @return bool Returns whether the format is valid. */ - protected function validateSelf() + public function validateStructure() { // there are no constraints by default return true; From 0372e5b406b901f80c6a62fc5ffacbebb167db9c Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 18 Dec 2024 14:06:33 +0100 Subject: [PATCH 009/103] format fields can now be attributed with request details --- .../presenters/base/BasePresenter.php | 41 +++++++++++++---- app/commands/MetaTester.php | 2 +- .../MetaFormats/FieldFormatDefinition.php | 5 +- app/helpers/MetaFormats/FormatAttribute.php | 2 +- .../FormatDefinitions/GroupFormat.php | 46 +++++++++---------- .../FormatDefinitions/UserFormat.php | 27 ++++++++++- app/helpers/MetaFormats/MetaFormatHelper.php | 23 +++++++++- app/helpers/MetaFormats/PhpTypes.php | 3 +- .../MetaFormats/PrimitiveFormatValidators.php | 18 +++++++- app/helpers/MetaFormats/RequestAttribute.php | 16 +++++++ app/helpers/MetaFormats/RequestParamData.php | 17 +++++++ app/helpers/MetaFormats/RequestParamType.php | 14 ++++++ 12 files changed, 174 insertions(+), 40 deletions(-) create mode 100644 app/helpers/MetaFormats/RequestAttribute.php create mode 100644 app/helpers/MetaFormats/RequestParamData.php create mode 100644 app/helpers/MetaFormats/RequestParamType.php diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 6443563ed..2066bb4d7 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -25,6 +25,7 @@ use App\Helpers\MetaFormats\FormatCache; use App\Helpers\MetaFormats\MetaFormat; use App\Helpers\MetaFormats\MetaRequest; +use App\Helpers\MetaFormats\RequestParamType; use App\Responses\StorageFileResponse; use App\Responses\ZipFilesResponse; use Nette\Application\Application; @@ -206,10 +207,6 @@ public function getMetaRequest(): MetaRequest|null private function processParams(ReflectionMethod $reflection) { - // $annotations = AnnotationsParser::getAll($reflection); - // $requiredFields = Arrays::get($annotations, "Param", []); - - ///TODO: add support for post/query type distinction $format = MetaFormatHelper::extractFormatFromAttribute($reflection); // ignore request that do not yet have the attribute @@ -217,15 +214,36 @@ private function processParams(ReflectionMethod $reflection) return; } + // get the parsed attribute data from the format fields + $formatToFieldDefinitionsMap = FormatCache::getFormatToFieldDefinitionsMap(); + if (!array_key_exists($format, $formatToFieldDefinitionsMap)) { + throw new InternalServerException("The format $format is not defined."); + } + + // maps field names to their attribute data + $nameToFieldDefinitionsMap = $formatToFieldDefinitionsMap[$format]; + ///TODO: handle nested MetaFormat creation - $fieldNames = FormatCache::getFormatFieldNames($format); $formatInstance = MetaFormatHelper::createFormatInstance($format); - foreach ($fieldNames as $field) { - ///TODO: check if required - $value = $this->getPostField($field, false); - if (!$formatInstance->checkedAssign($field, $value)) { + foreach ($nameToFieldDefinitionsMap as $fieldName => $fieldData) { + $requestParamData = $fieldData->requestData; + $this->logger->log(var_export($requestParamData, true), ILogger::DEBUG); + + $value = null; + switch ($requestParamData->type) { + case RequestParamType::Post: + $value = $this->getPostField($fieldName, required: $requestParamData->required); + break; + case RequestParamType::Query: + $value = $this->getQueryField($fieldName, required: $requestParamData->required); + break; + default: + throw new InternalServerException("Unknown parameter type: {$requestParamData->type}"); + } + + if (!$formatInstance->checkedAssign($fieldName, $value)) { ///TODO: it would be nice to give a more detailed error message here - throw new InvalidArgumentException($field); + throw new InvalidArgumentException($fieldName); } } @@ -236,6 +254,9 @@ private function processParams(ReflectionMethod $reflection) $this->requestFormatInstance = $formatInstance; + // $annotations = AnnotationsParser::getAll($reflection); + // $requiredFields = Arrays::get($annotations, "Param", []); + // $this->logger->log(var_export($annotations, true), ILogger::DEBUG); // $this->logger->log(var_export($requiredFields, true), ILogger::DEBUG); diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php index 5f767200a..5c87b454f 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaTester.php @@ -41,6 +41,6 @@ public function test(string $arg) // var_dump($format->checkIfAssignable("primaryAdminsIds", [ "10000000-2000-4000-8000-160000000000", "10000000-2000-4000-8000-160000000000" ])); $format = new UserFormat(); - $format->checkedAssign("titlesBeforeName", null); + var_dump($format->checkedAssign("email", "a@a.a.a")); } } diff --git a/app/helpers/MetaFormats/FieldFormatDefinition.php b/app/helpers/MetaFormats/FieldFormatDefinition.php index 1ee7f0e44..e17f08dee 100644 --- a/app/helpers/MetaFormats/FieldFormatDefinition.php +++ b/app/helpers/MetaFormats/FieldFormatDefinition.php @@ -10,6 +10,7 @@ class FieldFormatDefinition // A string name of the field type yielded by 'ReflectionProperty::getType()'. public ?string $type; public bool $nullable; + public RequestParamData $requestData; ///TODO: double check this private static array $gettypeToReflectiveMap = [ @@ -29,9 +30,10 @@ class FieldFormatDefinition * @param ?string $format The format of the field. * @param ?string $type The PHP type of the field yielded by a 'ReflectionProperty::getType()' call. * @param bool $nullable Whether the type is nullable. + * @param RequestParamData $requestData Request data such as param type and description. * @throws \App\Exceptions\InternalServerException Thrown when both @format and @type were null. */ - public function __construct(?string $format, ?string $type, bool $nullable) + public function __construct(?string $format, ?string $type, bool $nullable, RequestParamData $requestData) { // if both are null, there is no way to validate an assigned value if ($format === null && $type === null) { @@ -41,6 +43,7 @@ public function __construct(?string $format, ?string $type, bool $nullable) $this->format = $format; $this->type = $type; $this->nullable = $nullable; + $this->requestData = $requestData; } /** diff --git a/app/helpers/MetaFormats/FormatAttribute.php b/app/helpers/MetaFormats/FormatAttribute.php index 67d11df0d..c3ef84dd8 100644 --- a/app/helpers/MetaFormats/FormatAttribute.php +++ b/app/helpers/MetaFormats/FormatAttribute.php @@ -10,7 +10,7 @@ #[Attribute] class FormatAttribute { - public function __construct($format) + public function __construct(string $format) { } } diff --git a/app/helpers/MetaFormats/FormatDefinitions/GroupFormat.php b/app/helpers/MetaFormats/FormatDefinitions/GroupFormat.php index 284b8589e..2281c0c36 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/GroupFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/GroupFormat.php @@ -8,27 +8,27 @@ #[FormatAttribute("group")] class GroupFormat extends MetaFormat { - #[FormatAttribute("uuid")] - public string $id; - #[FormatAttribute("uuid")] - public string $externalId; - public bool $organizational; - public bool $exam; - public bool $archived; - public bool $public; - public bool $directlyArchived; - #[FormatAttribute("localizedText[]")] - public array $localizedTexts; - #[FormatAttribute("uuid[]")] - public array $primaryAdminsIds; - #[FormatAttribute("uuid?")] - public string $parentGroupId; - #[FormatAttribute("uuid[]")] - public array $parentGroupsIds; - #[FormatAttribute("uuid[]")] - public array $childGroups; - #[FormatAttribute("groupPrivateData")] - public $privateData; - #[FormatAttribute("acl[]")] - public array $permissionHints; + // #[FormatAttribute("uuid")] + // public string $id; + // #[FormatAttribute("uuid")] + // public string $externalId; + // public bool $organizational; + // public bool $exam; + // public bool $archived; + // public bool $public; + // public bool $directlyArchived; + // #[FormatAttribute("localizedText[]")] + // public array $localizedTexts; + // #[FormatAttribute("uuid[]")] + // public array $primaryAdminsIds; + // #[FormatAttribute("uuid?")] + // public string $parentGroupId; + // #[FormatAttribute("uuid[]")] + // public array $parentGroupsIds; + // #[FormatAttribute("uuid[]")] + // public array $childGroups; + // #[FormatAttribute("groupPrivateData")] + // public $privateData; + // #[FormatAttribute("acl[]")] + // public array $permissionHints; } diff --git a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php index 7ad250f16..fb201c013 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php @@ -4,17 +4,42 @@ use App\Helpers\MetaFormats\FormatAttribute; use App\Helpers\MetaFormats\MetaFormat; +use App\Helpers\MetaFormats\RequestAttribute; +use App\Helpers\MetaFormats\RequestParamType; #[FormatAttribute("userRegistration")] class UserFormat extends MetaFormat { - //#[FormatAttribute("email")] + #[FormatAttribute("email")] + #[RequestAttribute(type: RequestParamType::Post, description: "An email that will serve as a login name")] public string $email; + + #[RequestAttribute(type: RequestParamType::Post, description: "First name")] public string $firstName; + + #[RequestAttribute(type: RequestParamType::Post, description: "Last name")] public string $lastName; + + #[RequestAttribute(type: RequestParamType::Post, description: "A password for authentication")] public string $password; + + #[RequestAttribute(type: RequestParamType::Post, description: "A password confirmation")] public string $passwordConfirm; + + #[RequestAttribute(type: RequestParamType::Post, description: "Identifier of the instance to register in")] public string $instanceId; + + #[RequestAttribute( + type: RequestParamType::Post, + description: "Titles that are placed before user name", + required: false + )] public ?string $titlesBeforeName; + + #[RequestAttribute( + type: RequestParamType::Post, + description: "Titles that are placed after user name", + required: false + )] public ?string $titlesAfterName; } diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index a9fa79b6a..d0cf2eb66 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -84,6 +84,22 @@ public static function extractFormatFromAttribute( return $formatArguments[0]; } + public static function extractRequestAttributeData( + ReflectionClass|ReflectionProperty|ReflectionMethod $reflectionObject + ): ?RequestParamData { + $requestAttribute = $reflectionObject->getAttributes(RequestAttribute::class); + if (count($requestAttribute) === 0) { + return null; + } + + $requestArguments = $requestAttribute[0]->getArguments(); + $type = $requestArguments["type"]; + $description = array_key_exists("description", $requestArguments) ? $requestArguments["description"] : ""; + $required = array_key_exists("required", $requestArguments) ? $requestArguments["required"] : true; + + return new RequestParamData($type, $description, $required); + } + /** * Parses the format attributes of class fields and returns their metadata. * @param string $className The name of the class. @@ -103,7 +119,12 @@ public static function createNameToFieldDefinitionsMap(string $className) $fieldType = $reflectionType?->getName(); $nullable = $reflectionType?->allowsNull() ?? false; - $formats[$fieldName] = new FieldFormatDefinition($format, $fieldType, $nullable); + $requestParamData = self::extractRequestAttributeData($field); + if ($requestParamData === null) { + throw new InternalServerException("The field $fieldName of class $className does not have a RequestAttribute."); + } + + $formats[$fieldName] = new FieldFormatDefinition($format, $fieldType, $nullable, $requestParamData); } return $formats; diff --git a/app/helpers/MetaFormats/PhpTypes.php b/app/helpers/MetaFormats/PhpTypes.php index 9bebbc15a..2a60fc449 100644 --- a/app/helpers/MetaFormats/PhpTypes.php +++ b/app/helpers/MetaFormats/PhpTypes.php @@ -4,7 +4,8 @@ // the string values have to match the return string of gettype() // @codingStandardsIgnoreStart -enum PhpTypes: string { +enum PhpTypes: string +{ case String = "string"; case Int = "integer"; case Double = "double"; diff --git a/app/helpers/MetaFormats/PrimitiveFormatValidators.php b/app/helpers/MetaFormats/PrimitiveFormatValidators.php index c5076a1b0..b47a1fb03 100644 --- a/app/helpers/MetaFormats/PrimitiveFormatValidators.php +++ b/app/helpers/MetaFormats/PrimitiveFormatValidators.php @@ -7,7 +7,7 @@ class PrimitiveFormatValidators /** * @format uuid */ - public function validateUuid($uuid): bool + public function validateUuid(string $uuid): bool { if (!self::checkType($uuid, PhpTypes::String)) { return false; @@ -16,6 +16,22 @@ public function validateUuid($uuid): bool return preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $uuid) === 1; } + /** + * @format email + */ + public function validateEmail(string $email): bool + { + if (!self::checkType($email, PhpTypes::String)) { + return false; + } + + if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) { + return false; + } + + return true; + } + private static function checkType($value, PhpTypes $type): bool { return gettype($value) === $type->value; diff --git a/app/helpers/MetaFormats/RequestAttribute.php b/app/helpers/MetaFormats/RequestAttribute.php new file mode 100644 index 000000000..538c5455b --- /dev/null +++ b/app/helpers/MetaFormats/RequestAttribute.php @@ -0,0 +1,16 @@ +type = $type; + $this->description = $description; + $this->required = $required; + } +} diff --git a/app/helpers/MetaFormats/RequestParamType.php b/app/helpers/MetaFormats/RequestParamType.php new file mode 100644 index 000000000..735d487bb --- /dev/null +++ b/app/helpers/MetaFormats/RequestParamType.php @@ -0,0 +1,14 @@ + Date: Fri, 3 Jan 2025 12:25:54 +0100 Subject: [PATCH 010/103] renamed RequestAttribute to FormatParameterAttribute --- .../FormatDefinitions/UserFormat.php | 18 +++++++++--------- ...ribute.php => FormatParameterAttribute.php} | 4 ++-- app/helpers/MetaFormats/MetaFormatHelper.php | 6 ++++-- 3 files changed, 15 insertions(+), 13 deletions(-) rename app/helpers/MetaFormats/{RequestAttribute.php => FormatParameterAttribute.php} (68%) diff --git a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php index fb201c013..2f42d81ff 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php @@ -4,39 +4,39 @@ use App\Helpers\MetaFormats\FormatAttribute; use App\Helpers\MetaFormats\MetaFormat; -use App\Helpers\MetaFormats\RequestAttribute; +use App\Helpers\MetaFormats\FormatParameterAttribute; use App\Helpers\MetaFormats\RequestParamType; #[FormatAttribute("userRegistration")] class UserFormat extends MetaFormat { #[FormatAttribute("email")] - #[RequestAttribute(type: RequestParamType::Post, description: "An email that will serve as a login name")] + #[FormatParameterAttribute(type: RequestParamType::Post, description: "An email that will serve as a login name")] public string $email; - #[RequestAttribute(type: RequestParamType::Post, description: "First name")] + #[FormatParameterAttribute(type: RequestParamType::Post, description: "First name")] public string $firstName; - #[RequestAttribute(type: RequestParamType::Post, description: "Last name")] + #[FormatParameterAttribute(type: RequestParamType::Post, description: "Last name")] public string $lastName; - #[RequestAttribute(type: RequestParamType::Post, description: "A password for authentication")] + #[FormatParameterAttribute(type: RequestParamType::Post, description: "A password for authentication")] public string $password; - #[RequestAttribute(type: RequestParamType::Post, description: "A password confirmation")] + #[FormatParameterAttribute(type: RequestParamType::Post, description: "A password confirmation")] public string $passwordConfirm; - #[RequestAttribute(type: RequestParamType::Post, description: "Identifier of the instance to register in")] + #[FormatParameterAttribute(type: RequestParamType::Post, description: "Identifier of the instance to register in")] public string $instanceId; - #[RequestAttribute( + #[FormatParameterAttribute( type: RequestParamType::Post, description: "Titles that are placed before user name", required: false )] public ?string $titlesBeforeName; - #[RequestAttribute( + #[FormatParameterAttribute( type: RequestParamType::Post, description: "Titles that are placed after user name", required: false diff --git a/app/helpers/MetaFormats/RequestAttribute.php b/app/helpers/MetaFormats/FormatParameterAttribute.php similarity index 68% rename from app/helpers/MetaFormats/RequestAttribute.php rename to app/helpers/MetaFormats/FormatParameterAttribute.php index 538c5455b..4bd5b929b 100644 --- a/app/helpers/MetaFormats/RequestAttribute.php +++ b/app/helpers/MetaFormats/FormatParameterAttribute.php @@ -5,10 +5,10 @@ use Attribute; /** - * Attribute for request parameter details. + * Attribute used to annotate format definition class fields. */ #[Attribute] -class RequestAttribute +class FormatParameterAttribute { public function __construct(RequestParamType $type, string $description = "", bool $required = true) { diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index d0cf2eb66..056f4dd02 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -87,7 +87,7 @@ public static function extractFormatFromAttribute( public static function extractRequestAttributeData( ReflectionClass|ReflectionProperty|ReflectionMethod $reflectionObject ): ?RequestParamData { - $requestAttribute = $reflectionObject->getAttributes(RequestAttribute::class); + $requestAttribute = $reflectionObject->getAttributes(FormatParameterAttribute::class); if (count($requestAttribute) === 0) { return null; } @@ -121,7 +121,9 @@ public static function createNameToFieldDefinitionsMap(string $className) $requestParamData = self::extractRequestAttributeData($field); if ($requestParamData === null) { - throw new InternalServerException("The field $fieldName of class $className does not have a RequestAttribute."); + throw new InternalServerException( + "The field $fieldName of class $className does not have a RequestAttribute." + ); } $formats[$fieldName] = new FieldFormatDefinition($format, $fieldType, $nullable, $requestParamData); From 8d9d52d32001697689030cfb9f9305add68f530f Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sat, 4 Jan 2025 18:31:09 +0100 Subject: [PATCH 011/103] added attribute to substitue @Param, added script to convert files with @Param to attributes --- .../presenters/RegistrationPresenter.php | 5 +- .../presenters/base/BasePresenter.php | 3 + app/commands/MetaTester.php | 39 +++++- .../AnnotationToAttributeConverter.php | 131 ++++++++++++++++++ .../{ => Attributes}/FormatAttribute.php | 2 +- .../FormatParameterAttribute.php | 3 +- .../MetaFormats/Attributes/ParamAttribute.php | 29 ++++ .../FormatDefinitions/GroupFormat.php | 2 +- .../FormatDefinitions/UserFormat.php | 4 +- app/helpers/MetaFormats/MetaFormatHelper.php | 17 +++ app/helpers/Swagger/ParenthesesBuilder.php | 2 +- 11 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 app/helpers/MetaFormats/AnnotationToAttributeConverter.php rename app/helpers/MetaFormats/{ => Attributes}/FormatAttribute.php (79%) rename app/helpers/MetaFormats/{ => Attributes}/FormatParameterAttribute.php (73%) create mode 100644 app/helpers/MetaFormats/Attributes/ParamAttribute.php diff --git a/app/V1Module/presenters/RegistrationPresenter.php b/app/V1Module/presenters/RegistrationPresenter.php index 4a2564fc2..4f0dc1254 100644 --- a/app/V1Module/presenters/RegistrationPresenter.php +++ b/app/V1Module/presenters/RegistrationPresenter.php @@ -21,7 +21,9 @@ use App\Helpers\EmailVerificationHelper; use App\Helpers\RegistrationConfig; use App\Helpers\InvitationHelper; -use App\Helpers\MetaFormats\FormatAttribute; +use App\Helpers\MetaFormats\Attributes\FormatAttribute; +use App\Helpers\MetaFormats\FormatDefinitions\UserFormat; +use App\Helpers\MetaFormats\Attributes\ParamAttribute; use App\Security\Roles; use App\Security\ACL\IUserPermissions; use App\Security\ACL\IGroupPermissions; @@ -164,6 +166,7 @@ public function checkCreateAccount() * @throws InvalidArgumentException */ #[FormatAttribute("userRegistration")] + #[ParamAttribute("email", "An email that will serve as a login name", validation: [ new UserFormat() ])] public function actionCreateAccount() { $req = $this->getMetaRequest(); diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index dc60fcdbf..9b68a8065 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -215,6 +215,9 @@ private function processParams(ReflectionMethod $reflection) { $format = MetaFormatHelper::extractFormatFromAttribute($reflection); + $this->logger->log(var_export(MetaFormatHelper::debugGetAttributes($reflection), true), ILogger::DEBUG); + + // ignore request that do not yet have the attribute if ($format === null) { return; diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php index 5c87b454f..1e0298892 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaTester.php @@ -2,6 +2,7 @@ namespace App\Console; +use App\Helpers\MetaFormats\AnnotationToAttributeConverter; use App\Helpers\MetaFormats\FormatDefinitions\GroupFormat; use App\Helpers\MetaFormats\FormatDefinitions\UserFormat; use Symfony\Component\Console\Command\Command; @@ -40,7 +41,41 @@ public function test(string $arg) // $format = new GroupFormat(); // var_dump($format->checkIfAssignable("primaryAdminsIds", [ "10000000-2000-4000-8000-160000000000", "10000000-2000-4000-8000-160000000000" ])); - $format = new UserFormat(); - var_dump($format->checkedAssign("email", "a@a.a.a")); + // $format = new UserFormat(); + // var_dump($format->checkedAssign("email", "a@a.a.a")); + + $inDir = __DIR__ . "/../V1Module/presenters"; + $outDir = __DIR__ . "/../V1Module/presenters2"; + + // create output folder + if (!is_dir($outDir)) { + mkdir($outDir); + + // copy base subfolder + $inBaseDir = $inDir . "/base"; + $outBaseDir = $outDir . "/base"; + mkdir($outBaseDir); + $baseFilenames = scandir($inBaseDir); + foreach ($baseFilenames as $filename) { + if (!str_ends_with($filename, ".php")) { + continue; + } + + copy($inBaseDir . "/" . $filename, $outBaseDir); + } + } + + $filenames = scandir($inDir); + foreach ($filenames as $filename) { + if (!str_ends_with($filename, "Presenter.php")) { + continue; + } + + $filepath = $inDir . "/" . $filename; + $newContent = AnnotationToAttributeConverter::convertFile($filepath); + $newFile = fopen($outDir . "/" . $filename, "w"); + fwrite($newFile, $newContent); + fclose($newFile); + } } } diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php new file mode 100644 index 000000000..3845b9163 --- /dev/null +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -0,0 +1,131 @@ +addValue($type); + + // add name + if (!array_key_exists("name", $annotationParameters)) { + throw new InternalServerException("Missing name parameter."); + } + $parenthesesBuilder->addValue("\"{$annotationParameters["name"]}\""); + + if (array_key_exists("description", $annotationParameters)) { + $parenthesesBuilder->addValue("description: \"{$annotationParameters["description"]}\""); + } + + if (array_key_exists("validation", $annotationParameters)) { + ///TODO + $parenthesesBuilder->addValue("validation: [ \"{$annotationParameters["validation"]}\" ]"); + } + + if (array_key_exists("required", $annotationParameters)) { + $parenthesesBuilder->addValue("required: " . $annotationParameters["required"]); + } + + if (!array_key_exists("type", $annotationParameters)) { + throw new InternalServerException("Missing type parameter."); + } + + return "#[ParamAttribute{$parenthesesBuilder->toString()}]"; + } + + public static function convertFile(string $path) + { + // read file and replace @Param annotations with attributes + $content = file_get_contents($path); + $withInterleavedAttributes = preg_replace_callback(self::$postRegex, function ($matches) { + return self::regexCaptureToAttributeCallback($matches); + }, $content, -1, $count, PREG_UNMATCHED_AS_NULL); + + // move the attribute lines below the comment block + $lines = []; + $attributeLinesBuffer = []; + $usingsAdded = false; + foreach (preg_split("/((\r?\n)|(\r\n?))/", $withInterleavedAttributes) as $line) { + // add usings for new types + if (!$usingsAdded && strlen($line) > 3 && substr($line, 0, 3) === "use") { + $lines[] = "use App\Helpers\MetaFormats\Attributes\ParamAttribute;"; + $lines[] = "use App\Helpers\MetaFormats\RequestParamType;"; + $lines[] = $line; + $usingsAdded = true; + // store attribute lines in the buffer and do not write them + } elseif (preg_match("/#\[ParamAttribute/", $line) === 1) { + $attributeLinesBuffer[] = $line; + // flush attribute lines + } elseif (trim($line) === "*/") { + $lines[] = $line; + foreach ($attributeLinesBuffer as $attributeLine) { + // the attribute lines are shifted by one space to the right (due to the comment block origin) + $lines[] = substr($attributeLine, 1); + } + $attributeLinesBuffer = []; + } else { + $lines[] = $line; + } + } + + ///TODO: add usings for used validators + ///TODO: handle too long lines + return implode("\n", $lines); + } +} diff --git a/app/helpers/MetaFormats/FormatAttribute.php b/app/helpers/MetaFormats/Attributes/FormatAttribute.php similarity index 79% rename from app/helpers/MetaFormats/FormatAttribute.php rename to app/helpers/MetaFormats/Attributes/FormatAttribute.php index c3ef84dd8..37f5dd91c 100644 --- a/app/helpers/MetaFormats/FormatAttribute.php +++ b/app/helpers/MetaFormats/Attributes/FormatAttribute.php @@ -1,6 +1,6 @@ getAttributes(); + $data = []; + foreach ($requestAttributes as $attr) { + $data[] = $attr->getArguments(); + } + return $data; + } + /** * Parses the format attributes of class fields and returns their metadata. * @param string $className The name of the class. diff --git a/app/helpers/Swagger/ParenthesesBuilder.php b/app/helpers/Swagger/ParenthesesBuilder.php index ef744785f..ed5c23746 100644 --- a/app/helpers/Swagger/ParenthesesBuilder.php +++ b/app/helpers/Swagger/ParenthesesBuilder.php @@ -48,6 +48,6 @@ public function addValue(string $value): ParenthesesBuilder public function toString(): string { - return '(' . implode(',', $this->tokens) . ')'; + return '(' . implode(', ', $this->tokens) . ')'; } } From b9318032d72dadd7b4e8c776556f2a569f567d33 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 5 Jan 2025 12:41:46 +0100 Subject: [PATCH 012/103] added validators, added extraction method for RequestParamAttribute metadata --- .../presenters/RegistrationPresenter.php | 1 - app/commands/MetaTester.php | 10 ++- .../AnnotationToAttributeConverter.php | 69 +++++++++++++++++-- ...ttribute.php => RequestParamAttribute.php} | 15 +++- app/helpers/MetaFormats/MetaFormatHelper.php | 26 +++++++ .../MetaFormats/PrimitiveFormatValidators.php | 2 +- app/helpers/MetaFormats/RequestParamData.php | 4 +- .../MetaFormats/Validators/EmailValidator.php | 23 +++++++ .../Validators/StringValidator.php | 42 +++++++++++ .../MetaFormats/Validators/UuidValidator.php | 19 +++++ app/helpers/Swagger/AnnotationHelper.php | 2 +- 11 files changed, 201 insertions(+), 12 deletions(-) rename app/helpers/MetaFormats/Attributes/{ParamAttribute.php => RequestParamAttribute.php} (65%) create mode 100644 app/helpers/MetaFormats/Validators/EmailValidator.php create mode 100644 app/helpers/MetaFormats/Validators/StringValidator.php create mode 100644 app/helpers/MetaFormats/Validators/UuidValidator.php diff --git a/app/V1Module/presenters/RegistrationPresenter.php b/app/V1Module/presenters/RegistrationPresenter.php index 4f0dc1254..bf7082be9 100644 --- a/app/V1Module/presenters/RegistrationPresenter.php +++ b/app/V1Module/presenters/RegistrationPresenter.php @@ -166,7 +166,6 @@ public function checkCreateAccount() * @throws InvalidArgumentException */ #[FormatAttribute("userRegistration")] - #[ParamAttribute("email", "An email that will serve as a login name", validation: [ new UserFormat() ])] public function actionCreateAccount() { $req = $this->getMetaRequest(); diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php index 1e0298892..612d684d6 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaTester.php @@ -5,6 +5,8 @@ use App\Helpers\MetaFormats\AnnotationToAttributeConverter; use App\Helpers\MetaFormats\FormatDefinitions\GroupFormat; use App\Helpers\MetaFormats\FormatDefinitions\UserFormat; +use App\Helpers\MetaFormats\MetaFormatHelper; +use App\Helpers\Swagger\AnnotationHelper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -44,6 +46,7 @@ public function test(string $arg) // $format = new UserFormat(); // var_dump($format->checkedAssign("email", "a@a.a.a")); + /* $inDir = __DIR__ . "/../V1Module/presenters"; $outDir = __DIR__ . "/../V1Module/presenters2"; @@ -61,7 +64,7 @@ public function test(string $arg) continue; } - copy($inBaseDir . "/" . $filename, $outBaseDir); + copy($inBaseDir . "/" . $filename, $outBaseDir . "/" . $filename); } } @@ -77,5 +80,10 @@ public function test(string $arg) fwrite($newFile, $newContent); fclose($newFile); } + */ + + $reflection = AnnotationHelper::getMethod("App\V1Module\Presenters\RegistrationPresenter", "actionCreateAccount"); + $attrs = MetaFormatHelper::extractRequestParamData($reflection); + var_dump($attrs); } } diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index 3845b9163..5d4dd2caf 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -74,8 +74,8 @@ private static function regexCaptureToAttributeCallback(array $matches) } if (array_key_exists("validation", $annotationParameters)) { - ///TODO - $parenthesesBuilder->addValue("validation: [ \"{$annotationParameters["validation"]}\" ]"); + $validator = self::convertAnnotationValidationToValidatorString($annotationParameters["validation"]); + $parenthesesBuilder->addValue("validators: [ $validator ]"); } if (array_key_exists("required", $annotationParameters)) { @@ -86,7 +86,63 @@ private static function regexCaptureToAttributeCallback(array $matches) throw new InternalServerException("Missing type parameter."); } - return "#[ParamAttribute{$parenthesesBuilder->toString()}]"; + return "#[RequestParamAttribute{$parenthesesBuilder->toString()}]"; + } + + /** + * Converts annotation validation values (such as "string:1..255") to Validator construction + * strings (such as "new StringValidator(1, 255)"). + * @param string $validation The annotation validation string. + * @return string Returns the object construction string. + */ + private static function convertAnnotationValidationToValidatorString(string $validation): string + { + if (str_starts_with($validation, "string")) { + // handle string length constraints, such as "string:1..255" + if (strlen($validation) > 6) { + if ($validation[6] !== ":") { + throw new InternalServerException("Unknown string validation format: $validation"); + } + $suffix = substr($validation, 7); + + // special case for uuids + if ($suffix === "36") { + return "new UuidValidator()"; + } + + // capture the two bounding numbers and the double dot in strings of + // types "1..255", "..255", "1..", or "255" + if (preg_match("/([0-9]*)(..)?([0-9]+)?/", $suffix, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InternalServerException("Unknown string validation format: $validation"); + } + + // type "255", exact match + if ($matches[2] === null) { + return "new StringValidator({$matches[1]}, {$matches[1]})"; + // type "1..255" + } elseif ($matches[1] !== null && $matches[3] !== null) { + return "new StringValidator({$matches[1]}, {$matches[3]})"; + // type "..255" + } elseif ($matches[1] === null) { + return "new StringValidator(0, {$matches[3]})"; + // type "1.." + } elseif ($matches[3] === null) { + return "new StringValidator({$matches[1]})"; + } + + throw new InternalServerException("Unknown string validation format: $validation"); + } + + return "new StringValidator()"; + } + + switch ($validation) { + case "email": + return "new EmailValidator()"; + default: + ///TODO + return "\"UNSUPPORTED\""; + } } public static function convertFile(string $path) @@ -104,12 +160,15 @@ public static function convertFile(string $path) foreach (preg_split("/((\r?\n)|(\r\n?))/", $withInterleavedAttributes) as $line) { // add usings for new types if (!$usingsAdded && strlen($line) > 3 && substr($line, 0, 3) === "use") { - $lines[] = "use App\Helpers\MetaFormats\Attributes\ParamAttribute;"; + $lines[] = "use App\Helpers\MetaFormats\Attributes\RequestParamAttribute;"; $lines[] = "use App\Helpers\MetaFormats\RequestParamType;"; + $lines[] = "use App\Helpers\MetaFormats\Validators\StringValidator;"; + $lines[] = "use App\Helpers\MetaFormats\Validators\EmailValidator;"; + $lines[] = "use App\Helpers\MetaFormats\Validators\UuidValidator;"; $lines[] = $line; $usingsAdded = true; // store attribute lines in the buffer and do not write them - } elseif (preg_match("/#\[ParamAttribute/", $line) === 1) { + } elseif (preg_match("/#\[RequestParamAttribute/", $line) === 1) { $attributeLinesBuffer[] = $line; // flush attribute lines } elseif (trim($line) === "*/") { diff --git a/app/helpers/MetaFormats/Attributes/ParamAttribute.php b/app/helpers/MetaFormats/Attributes/RequestParamAttribute.php similarity index 65% rename from app/helpers/MetaFormats/Attributes/ParamAttribute.php rename to app/helpers/MetaFormats/Attributes/RequestParamAttribute.php index 62dcd6813..17ae771cf 100644 --- a/app/helpers/MetaFormats/Attributes/ParamAttribute.php +++ b/app/helpers/MetaFormats/Attributes/RequestParamAttribute.php @@ -8,9 +8,15 @@ /** * Attribute used to annotate individual post or query parameters of endpoints. */ -#[Attribute] -class ParamAttribute +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)] +class RequestParamAttribute { + public RequestParamType $type; + public string $paramName; + public string $description; + public bool $required; + public array $validators; + /** * @param \App\Helpers\MetaFormats\RequestParamType $type The request parameter type (Post or Query). * @param string $name The name of the request parameter. @@ -25,5 +31,10 @@ public function __construct( bool $required = true, array $validators = [], ) { + $this->type = $type; + $this->paramName = $name; + $this->description = $description; + $this->required = $required; + $this->validators = $validators; } } diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 9fb33be9c..8f5a13efd 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -3,6 +3,10 @@ namespace App\Helpers\MetaFormats; use App\Exceptions\InternalServerException; +use App\Helpers\MetaFormats\Attributes\FormatAttribute; +use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute; +use App\Helpers\MetaFormats\Attributes\ParamAttribute; +use App\Helpers\MetaFormats\Attributes\RequestParamAttribute; use ReflectionClass; use App\Helpers\Swagger\AnnotationHelper; use ReflectionMethod; @@ -84,6 +88,28 @@ public static function extractFormatFromAttribute( return $formatArguments[0]; } + /** + * Fetches all attributes of a method and extracts the parameter data. + * @param \ReflectionMethod $reflectionMethod The method reflection object. + * @return array Returns an array of RequestParamData objects with the extracted data. + */ + public static function extractRequestParamData(ReflectionMethod $reflectionMethod): array + { + $attrs = $reflectionMethod->getAttributes(RequestParamAttribute::class); + $data = []; + foreach ($attrs as $attr) { + $paramAttr = $attr->newInstance(); + $type = $paramAttr->type; + $description = $paramAttr->description; + $required = $paramAttr->required; + $validators = $paramAttr->validators; + + $data[] = new RequestParamData($type, $description, $required, $validators); + } + + return $data; + } + public static function extractRequestAttributeData( ReflectionClass|ReflectionProperty|ReflectionMethod $reflectionObject ): ?RequestParamData { diff --git a/app/helpers/MetaFormats/PrimitiveFormatValidators.php b/app/helpers/MetaFormats/PrimitiveFormatValidators.php index b47a1fb03..4bf6446cb 100644 --- a/app/helpers/MetaFormats/PrimitiveFormatValidators.php +++ b/app/helpers/MetaFormats/PrimitiveFormatValidators.php @@ -32,7 +32,7 @@ public function validateEmail(string $email): bool return true; } - private static function checkType($value, PhpTypes $type): bool + public static function checkType($value, PhpTypes $type): bool { return gettype($value) === $type->value; } diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 3378dd2d5..9bdf419df 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -7,11 +7,13 @@ class RequestParamData public RequestParamType $type; public string $description; public bool $required; + public array $validators; - public function __construct(RequestParamType $type, string $description, bool $required) + public function __construct(RequestParamType $type, string $description, bool $required, array $validators = []) { $this->type = $type; $this->description = $description; $this->required = $required; + $this->validators = $validators; } } diff --git a/app/helpers/MetaFormats/Validators/EmailValidator.php b/app/helpers/MetaFormats/Validators/EmailValidator.php new file mode 100644 index 000000000..5d1bbc023 --- /dev/null +++ b/app/helpers/MetaFormats/Validators/EmailValidator.php @@ -0,0 +1,23 @@ +minLength = $minLength; + $this->maxLength = $maxLength; + $this->regex = $regex; + } + + public function validate(string $value) + { + if (!PrimitiveFormatValidators::checkType($value, PhpTypes::String)) { + return false; + } + + $length = strlen($value); + if ($length < $this->minLength) { + return false; + } + if ($this->maxLength !== -1 && $length > $this->maxLength) { + return false; + } + + if ($this->regex === null) { + return true; + } + + return preg_match($this->regex, $value) === 1; + } +} diff --git a/app/helpers/MetaFormats/Validators/UuidValidator.php b/app/helpers/MetaFormats/Validators/UuidValidator.php new file mode 100644 index 000000000..64df122fc --- /dev/null +++ b/app/helpers/MetaFormats/Validators/UuidValidator.php @@ -0,0 +1,19 @@ +getMethod($methodName); From d02392a513044d4edf67a34e2d1957a7f0e89dee Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 5 Jan 2025 14:17:35 +0100 Subject: [PATCH 013/103] changed some presenter descriptions to combat regex catastrophic backtracking --- app/V1Module/presenters/SubmitPresenter.php | 2 +- app/V1Module/presenters/UploadedFilesPresenter.php | 6 ++---- app/V1Module/presenters/UsersPresenter.php | 9 +++------ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/app/V1Module/presenters/SubmitPresenter.php b/app/V1Module/presenters/SubmitPresenter.php index 7b14cfb16..253a36537 100644 --- a/app/V1Module/presenters/SubmitPresenter.php +++ b/app/V1Module/presenters/SubmitPresenter.php @@ -342,7 +342,7 @@ public function checkResubmit(string $id) * @POST * @param string $id Identifier of the solution * @Param(type="post", name="debug", validation="bool", required=false, - * "Debugging resubmit with all logs and outputs") + * description="Debugging resubmit with all logs and outputs") * @throws ForbiddenRequestException * @throws InvalidArgumentException * @throws NotFoundException diff --git a/app/V1Module/presenters/UploadedFilesPresenter.php b/app/V1Module/presenters/UploadedFilesPresenter.php index 65a99dbda..e27b8023d 100644 --- a/app/V1Module/presenters/UploadedFilesPresenter.php +++ b/app/V1Module/presenters/UploadedFilesPresenter.php @@ -168,8 +168,7 @@ public function checkDownload(string $id, ?string $entry = null, ?string $simila * @Param(type="query", name="entry", required=false, validation="string:1..", * description="Name of the entry in the ZIP archive (if the target file is ZIP)") * @Param(type="query", name="similarSolutionId", required=false, validation="string:36", - * description="Id of an assignment solution which has detected possible plagiarism in this file. - * This is basically a shortcut (hint) for ACLs.") + * description="Id of an assignment solution which has detected possible plagiarism in this file. This is basically a shortcut (hint) for ACLs.") * @throws \Nette\Application\AbortException * @throws \Nette\Application\BadRequestException */ @@ -210,8 +209,7 @@ public function checkContent(string $id, ?string $entry = null, ?string $similar * @Param(type="query", name="entry", required=false, validation="string:1..", * description="Name of the entry in the ZIP archive (if the target file is ZIP)") * @Param(type="query", name="similarSolutionId", required=false, validation="string:36", - * description="Id of an assignment solution which has detected possible plagiarism in this file. - * This is basically a shortcut (hint) for ACLs.") + * description="Id of an assignment solution which has detected possible plagiarism in this file. This is basically a shortcut (hint) for ACLs.") */ public function actionContent(string $id, ?string $entry = null) { diff --git a/app/V1Module/presenters/UsersPresenter.php b/app/V1Module/presenters/UsersPresenter.php index b15565040..8b2b03518 100644 --- a/app/V1Module/presenters/UsersPresenter.php +++ b/app/V1Module/presenters/UsersPresenter.php @@ -434,18 +434,15 @@ public function checkUpdateSettings(string $id) * @Param(type="post", name="pointsChangedEmails", validation="bool", required=false, * description="Flag if email should be sent to user when the points were awarded for assignment") * @Param(type="post", name="assignmentSubmitAfterAcceptedEmails", validation="bool", required=false, - * description="Flag if email should be sent to group supervisor if a student submits new solution - * for already accepted assignment") + * description="Flag if email should be sent to group supervisor if a student submits new solution for already accepted assignment") * @Param(type="post", name="assignmentSubmitAfterReviewedEmails", validation="bool", required=false, - * description="Flag if email should be sent to group supervisor if a student submits new solution - * for already reviewed and not accepted assignment") + * description="Flag if email should be sent to group supervisor if a student submits new solution for already reviewed and not accepted assignment") * @Param(type="post", name="exerciseNotificationEmails", validation="bool", required=false, * description="Flag if notifications sent by authors of exercises should be sent via email.") * @Param(type="post", name="solutionAcceptedEmails", validation="bool", required=false, * description="Flag if notification should be sent to a student when solution accepted flag is changed.") * @Param(type="post", name="solutionReviewRequestedEmails", validation="bool", required=false, - * description="Flag if notification should be send to a teacher when a solution reviewRequested flag - * is chagned in a supervised/admined group.") + * description="Flag if notification should be send to a teacher when a solution reviewRequested flag is chagned in a supervised/admined group.") * @throws NotFoundException */ public function actionUpdateSettings(string $id) From 73ee0a1172b4f2e6dd0ab641794ced1502b74574 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 5 Jan 2025 14:21:26 +0100 Subject: [PATCH 014/103] changed presenter description to avoid a bug in the attribute converter regex --- app/V1Module/presenters/ExercisesPresenter.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/V1Module/presenters/ExercisesPresenter.php b/app/V1Module/presenters/ExercisesPresenter.php index 15c01613d..309600405 100644 --- a/app/V1Module/presenters/ExercisesPresenter.php +++ b/app/V1Module/presenters/ExercisesPresenter.php @@ -757,8 +757,7 @@ public function checkTagsUpdateGlobal(string $tag) * @Param(type="query", name="renameTo", validation="string:1..32", required=false, * description="New name of the tag") * @Param(type="query", name="force", validation="bool", required=false, - * description="If true, the rename will be allowed even if the new tag name exists (tags will be merged). - * Otherwise, name collisions will result in error.") + * description="If true, the rename will be allowed even if the new tag name exists (tags will be merged). Otherwise, name collisions will result in error.") */ public function actionTagsUpdateGlobal(string $tag, string $renameTo = null, bool $force = false) { From ba23bfedb8f0fb448db36e2c0bc2ee3eef601806 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 5 Jan 2025 14:37:12 +0100 Subject: [PATCH 015/103] the swagger generator is now attribute-based, added empty validators for remaining types --- app/commands/MetaTester.php | 8 ++--- .../AnnotationToAttributeConverter.php | 25 ++++++++++++++++ app/helpers/MetaFormats/MetaFormatHelper.php | 15 +++++----- app/helpers/MetaFormats/RequestParamData.php | 29 +++++++++++++++++-- .../MetaFormats/Validators/ArrayValidator.php | 17 +++++++++++ .../MetaFormats/Validators/BoolValidator.php | 17 +++++++++++ .../MetaFormats/Validators/FloatValidator.php | 17 +++++++++++ .../MetaFormats/Validators/IntValidator.php | 17 +++++++++++ .../Validators/TimestampValidator.php | 17 +++++++++++ app/helpers/Swagger/AnnotationHelper.php | 14 ++++++--- .../Swagger/AnnotationParameterData.php | 4 +-- 11 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 app/helpers/MetaFormats/Validators/ArrayValidator.php create mode 100644 app/helpers/MetaFormats/Validators/BoolValidator.php create mode 100644 app/helpers/MetaFormats/Validators/FloatValidator.php create mode 100644 app/helpers/MetaFormats/Validators/IntValidator.php create mode 100644 app/helpers/MetaFormats/Validators/TimestampValidator.php diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php index 612d684d6..1482ac5d3 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaTester.php @@ -46,7 +46,6 @@ public function test(string $arg) // $format = new UserFormat(); // var_dump($format->checkedAssign("email", "a@a.a.a")); - /* $inDir = __DIR__ . "/../V1Module/presenters"; $outDir = __DIR__ . "/../V1Module/presenters2"; @@ -80,10 +79,9 @@ public function test(string $arg) fwrite($newFile, $newContent); fclose($newFile); } - */ - $reflection = AnnotationHelper::getMethod("App\V1Module\Presenters\RegistrationPresenter", "actionCreateAccount"); - $attrs = MetaFormatHelper::extractRequestParamData($reflection); - var_dump($attrs); + // $reflection = AnnotationHelper::getMethod("App\V1Module\Presenters\RegistrationPresenter", "actionCreateAccount"); + // $attrs = MetaFormatHelper::extractRequestParamData($reflection); + // var_dump($attrs); } } diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index 5d4dd2caf..f0e3cc0f4 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -136,11 +136,31 @@ private static function convertAnnotationValidationToValidatorString(string $val return "new StringValidator()"; } + ///TODO: this ignores nullability + if (str_ends_with($validation, "|null")) { + $validation = substr($validation, 0, -5); + } + switch ($validation) { case "email": + // there is one occurrence of this + case "email:1..": return "new EmailValidator()"; + case "numericint": + return "new IntValidator()"; + case "bool": + case "boolean": + return "new BoolValidator()"; + case "array": + case "list": + return "new ArrayValidator()"; + case "timestamp": + return "new TimestampValidator()"; + case "numeric": + return "new FloatValidator()"; default: ///TODO + var_dump("unsupported: $validation"); return "\"UNSUPPORTED\""; } } @@ -165,6 +185,11 @@ public static function convertFile(string $path) $lines[] = "use App\Helpers\MetaFormats\Validators\StringValidator;"; $lines[] = "use App\Helpers\MetaFormats\Validators\EmailValidator;"; $lines[] = "use App\Helpers\MetaFormats\Validators\UuidValidator;"; + $lines[] = "use App\Helpers\MetaFormats\Validators\BoolValidator;"; + $lines[] = "use App\Helpers\MetaFormats\Validators\ArrayValidator;"; + $lines[] = "use App\Helpers\MetaFormats\Validators\FloatValidator;"; + $lines[] = "use App\Helpers\MetaFormats\Validators\IntValidator;"; + $lines[] = "use App\Helpers\MetaFormats\Validators\TimestampValidator;"; $lines[] = $line; $usingsAdded = true; // store attribute lines in the buffer and do not write them diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 8f5a13efd..6f43fc5f5 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -99,12 +99,13 @@ public static function extractRequestParamData(ReflectionMethod $reflectionMetho $data = []; foreach ($attrs as $attr) { $paramAttr = $attr->newInstance(); - $type = $paramAttr->type; - $description = $paramAttr->description; - $required = $paramAttr->required; - $validators = $paramAttr->validators; - - $data[] = new RequestParamData($type, $description, $required, $validators); + $data[] = new RequestParamData( + $paramAttr->type, + $paramAttr->paramName, + $paramAttr->description, + $paramAttr->required, + $paramAttr->validators + ); } return $data; @@ -123,7 +124,7 @@ public static function extractRequestAttributeData( $description = array_key_exists("description", $requestArguments) ? $requestArguments["description"] : ""; $required = array_key_exists("required", $requestArguments) ? $requestArguments["required"] : true; - return new RequestParamData($type, $description, $required); + return new RequestParamData($type, "TODO_IMPLEMENT_FOR_FormatParameterAttribute", $description, $required); } /** diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 9bdf419df..98b7df46b 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -2,18 +2,43 @@ namespace App\Helpers\MetaFormats; +use App\Helpers\MetaFormats\Validators\StringValidator; +use App\Helpers\Swagger\AnnotationParameterData; + class RequestParamData { public RequestParamType $type; + public string $name; public string $description; public bool $required; public array $validators; - public function __construct(RequestParamType $type, string $description, bool $required, array $validators = []) - { + public function __construct( + RequestParamType $type, + string $name, + string $description, + bool $required, + array $validators = [] + ) { $this->type = $type; + $this->name = $name; $this->description = $description; $this->required = $required; $this->validators = $validators; } + + public function toAnnotationParameterData() + { + $dataType = null; + if (count($this->validators) > 0) { + $dataType = $this->validators[0]::SWAGGER_TYPE; + } + + return new AnnotationParameterData( + $dataType, + $this->name, + $this->description, + strtolower($this->type->name) + ); + } } diff --git a/app/helpers/MetaFormats/Validators/ArrayValidator.php b/app/helpers/MetaFormats/Validators/ArrayValidator.php new file mode 100644 index 000000000..b905fd802 --- /dev/null +++ b/app/helpers/MetaFormats/Validators/ArrayValidator.php @@ -0,0 +1,17 @@ + $methodEnum) { - if (in_array($methodString, $annotations)) { - return $methodEnum; + foreach ($annotations as $annotation) { + if (str_starts_with($annotation, $methodString)) { + return $methodEnum; + } } } @@ -218,8 +221,11 @@ public static function extractAnnotationData(string $className, string $methodNa $httpMethod = self::extractAnnotationHttpMethod($methodAnnotations); $standardAnnotationParams = self::extractStandardAnnotationParams($methodAnnotations, $route); - $netteAnnotationParams = self::extractNetteAnnotationParams($methodAnnotations); - $params = array_merge($standardAnnotationParams, $netteAnnotationParams); + $attributeData = MetaFormatHelper::extractRequestParamData(self::getMethod($className, $methodName)); + $attributeParams = array_map(function ($data) { + return $data->toAnnotationParameterData(); + }, $attributeData); + $params = array_merge($standardAnnotationParams, $attributeParams); $pathParams = []; $queryParams = []; diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index d915f32d3..50a198502 100644 --- a/app/helpers/Swagger/AnnotationParameterData.php +++ b/app/helpers/Swagger/AnnotationParameterData.php @@ -78,14 +78,14 @@ private function getSwaggerType(): string if ($this->isDatatypeNullable()) { $typename = substr($typename, 0, -strlen(self::$nullableSuffix)); } - + if (self::$typeMap[$typename] === null) { ///TODO: Return the commented exception below once the meta-view formats are implemented. /// This detaults to strings because custom types like 'email' are not supported yet. return 'string'; } //throw new \InvalidArgumentException("Error in getSwaggerType: Unknown typename: {$typename}"); - + $type = self::$typeMap[$typename]; } return $type; From 19be3933241589af372d309540c2206981254474 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 5 Jan 2025 18:48:43 +0100 Subject: [PATCH 016/103] added support for loose attribute validation --- .../presenters/base/BasePresenter.php | 110 ++++++++++-------- app/helpers/MetaFormats/MetaFormatHelper.php | 3 +- 2 files changed, 64 insertions(+), 49 deletions(-) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 9b68a8065..c3e41bbed 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -24,6 +24,7 @@ use App\Helpers\MetaFormats\FormatCache; use App\Helpers\MetaFormats\MetaFormat; use App\Helpers\MetaFormats\MetaRequest; +use App\Helpers\MetaFormats\RequestParamData; use App\Helpers\MetaFormats\RequestParamType; use App\Responses\StorageFileResponse; use App\Responses\ZipFilesResponse; @@ -207,22 +208,59 @@ protected function isInScope(string $scope): bool public function getMetaRequest(): MetaRequest|null { + if ($this->requestFormatInstance === null) { + throw new InternalServerException( + "getMetaRequest() cannot be used without a format class defined for the endpoint." + ); + } + $request = parent::getRequest(); return new MetaRequest($request, $this->requestFormatInstance); } private function processParams(ReflectionMethod $reflection) { + $this->logger->log(var_export(MetaFormatHelper::debugGetAttributes($reflection), true), ILogger::DEBUG); + + // use a method specialized for formats if there is a format available $format = MetaFormatHelper::extractFormatFromAttribute($reflection); + if ($format !== null) { + $this->processParamsFormat($format); + return; + } - $this->logger->log(var_export(MetaFormatHelper::debugGetAttributes($reflection), true), ILogger::DEBUG); + // otherwise use a method for loose parameters + $paramData = MetaFormatHelper::extractRequestParamData($reflection); + $this->processParamsLoose($paramData); + } + private function processParamsLoose(array $paramData) + { + // validate each param + foreach ($paramData as $param) { + $paramValue = $this->getValueFromParamData($param); + + // check if null + if ($paramValue === null) { + if ($param->required) { + throw new InvalidArgumentException($param->name, "The parameter is required and cannot be null."); + } + + // only non null values should be validated + continue; + } - // ignore request that do not yet have the attribute - if ($format === null) { - return; + // use every provided validator + foreach ($param->validators as $validator) { + if (!$validator->validate($paramValue)) { + throw new InvalidArgumentException($param->name); + } + } } + } + private function processParamsFormat(string $format) + { // get the parsed attribute data from the format fields $formatToFieldDefinitionsMap = FormatCache::getFormatToFieldDefinitionsMap(); if (!array_key_exists($format, $formatToFieldDefinitionsMap)) { @@ -236,19 +274,9 @@ private function processParams(ReflectionMethod $reflection) $formatInstance = MetaFormatHelper::createFormatInstance($format); foreach ($nameToFieldDefinitionsMap as $fieldName => $fieldData) { $requestParamData = $fieldData->requestData; - $this->logger->log(var_export($requestParamData, true), ILogger::DEBUG); - - $value = null; - switch ($requestParamData->type) { - case RequestParamType::Post: - $value = $this->getPostField($fieldName, required: $requestParamData->required); - break; - case RequestParamType::Query: - $value = $this->getQueryField($fieldName, required: $requestParamData->required); - break; - default: - throw new InternalServerException("Unknown parameter type: {$requestParamData->type}"); - } + //$this->logger->log(var_export($requestParamData, true), ILogger::DEBUG); + + $value = $this->getValueFromParamData($requestParamData); if (!$formatInstance->checkedAssign($fieldName, $value)) { ///TODO: it would be nice to give a more detailed error message here @@ -262,38 +290,24 @@ private function processParams(ReflectionMethod $reflection) } $this->requestFormatInstance = $formatInstance; + } - // $annotations = AnnotationsParser::getAll($reflection); - // $requiredFields = Arrays::get($annotations, "Param", []); - - // $this->logger->log(var_export($annotations, true), ILogger::DEBUG); - // $this->logger->log(var_export($requiredFields, true), ILogger::DEBUG); - - // foreach ($requiredFields as $field) { - // $type = strtolower($field->type); - // $name = $field->name; - // $validationRule = isset($field->validation) ? $field->validation : null; - // $msg = isset($field->msg) ? $field->msg : null; - // $required = isset($field->required) ? $field->required : true; - - // $this->logger->log("test", ILogger::DEBUG); - - // $value = null; - // switch ($type) { - // case "post": - // $value = $this->getPostField($name, $required); - // break; - // case "query": - // $value = $this->getQueryField($name, $required); - // break; - // default: - // throw new InternalServerException("Unknown parameter type '$type'"); - // } - - // if ($validationRule !== null && $value !== null) { - // $value = $this->validateValue($name, $value, $validationRule, $msg); - // } - // } + /** + * Calls either getPostField or getQueryField 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. + */ + private function getValueFromParamData(RequestParamData $paramData): mixed + { + switch ($paramData->type) { + case RequestParamType::Post: + return $this->getPostField($paramData->name, required: $paramData->required); + case RequestParamType::Query: + return $this->getQueryField($paramData->name, required: $paramData->required); + default: + throw new InternalServerException("Unknown parameter type: {$paramData->type->name}"); + } } private function getPostField($param, $required = true) diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 6f43fc5f5..4427ed059 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -120,11 +120,12 @@ public static function extractRequestAttributeData( } $requestArguments = $requestAttribute[0]->getArguments(); + $name = $reflectionObject->name; $type = $requestArguments["type"]; $description = array_key_exists("description", $requestArguments) ? $requestArguments["description"] : ""; $required = array_key_exists("required", $requestArguments) ? $requestArguments["required"] : true; - return new RequestParamData($type, "TODO_IMPLEMENT_FOR_FormatParameterAttribute", $description, $required); + return new RequestParamData($type, $name, $description, $required); } /** From e49714833b9fc85cd58ee77b5c58e3681d4c61d9 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 23 Jan 2025 13:37:42 +0100 Subject: [PATCH 017/103] added fields to attributes for extraction, formats are now referenced with classnames, not hardcoded strings --- .../Attributes/FormatAttribute.php | 5 +- .../Attributes/FormatParameterAttribute.php | 23 ++++- app/helpers/MetaFormats/FormatCache.php | 8 -- .../FormatDefinitions/GroupFormat.php | 2 +- .../FormatDefinitions/UserFormat.php | 2 +- app/helpers/MetaFormats/MetaFormatHelper.php | 90 ++++++------------- .../MetaFormats/PrimitiveFormatValidators.php | 39 -------- .../Validators/StringValidator.php | 4 +- 8 files changed, 57 insertions(+), 116 deletions(-) delete mode 100644 app/helpers/MetaFormats/PrimitiveFormatValidators.php diff --git a/app/helpers/MetaFormats/Attributes/FormatAttribute.php b/app/helpers/MetaFormats/Attributes/FormatAttribute.php index 37f5dd91c..8c6558738 100644 --- a/app/helpers/MetaFormats/Attributes/FormatAttribute.php +++ b/app/helpers/MetaFormats/Attributes/FormatAttribute.php @@ -10,7 +10,10 @@ #[Attribute] class FormatAttribute { - public function __construct(string $format) + public string $class; + + public function __construct(string $class) { + $this->class = $class; } } diff --git a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php index 5905dbc4b..804f4ae29 100644 --- a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php +++ b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php @@ -11,7 +11,26 @@ #[Attribute] class FormatParameterAttribute { - public function __construct(RequestParamType $type, string $description = "", bool $required = true) - { + public RequestParamType $type; + public string $description; + public bool $required; + public array $validators; + + /** + * @param \App\Helpers\MetaFormats\RequestParamType $type The request parameter type (Post or Query). + * @param string $description The description of the request parameter. + * @param bool $required Whether the request parameter is required. + * @param array $validators An array of validators applied to the request parameter. + */ + public function __construct( + RequestParamType $type, + string $description = "", + bool $required = true, + array $validators = [], + ) { + $this->type = $type; + $this->description = $description; + $this->required = $required; + $this->validators = $validators; } } diff --git a/app/helpers/MetaFormats/FormatCache.php b/app/helpers/MetaFormats/FormatCache.php index 249b65922..6403c3bab 100644 --- a/app/helpers/MetaFormats/FormatCache.php +++ b/app/helpers/MetaFormats/FormatCache.php @@ -52,14 +52,6 @@ public static function getFormatFieldNames(string $format): array return array_keys($formatToFieldDefinitionsMap[$format]); } - public static function getValidators(): array - { - if (self::$validators == null) { - self::$validators = MetaFormatHelper::getValidators(); - } - return self::$validators; - } - public static function getFieldDefinitions(string $className) { $classToFormatMap = self::getClassToFormatMap(); diff --git a/app/helpers/MetaFormats/FormatDefinitions/GroupFormat.php b/app/helpers/MetaFormats/FormatDefinitions/GroupFormat.php index 99b5e8c1f..18b08c3a6 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/GroupFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/GroupFormat.php @@ -5,7 +5,7 @@ use App\Helpers\MetaFormats\Attributes\FormatAttribute; use App\Helpers\MetaFormats\MetaFormat; -#[FormatAttribute("group")] +#[FormatAttribute(GroupFormat::class)] class GroupFormat extends MetaFormat { // #[FormatAttribute("uuid")] diff --git a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php index 4e3920984..c417841ed 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php @@ -7,7 +7,7 @@ use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute; use App\Helpers\MetaFormats\RequestParamType; -#[FormatAttribute("userRegistration")] +#[FormatAttribute(UserFormat::class)] class UserFormat extends MetaFormat { #[FormatAttribute("email")] diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 4427ed059..3be083e0d 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -67,7 +67,6 @@ public static function extractMethodCheckedParams(string $className, string $met * Checks whether an entity contains a FormatAttribute and extracts the format if so. * @param \ReflectionClass|\ReflectionProperty|\ReflectionMethod $reflectionObject A reflection * object of the entity. - * @throws \App\Exceptions\InternalServerException Thrown when the FormatAttribute was used incorrectly. * @return ?string Returns the format or null if no FormatAttribute was present. */ public static function extractFormatFromAttribute( @@ -78,14 +77,8 @@ public static function extractFormatFromAttribute( return null; } - // check attribute correctness - $formatArguments = $formatAttributes[0]->getArguments(); - if (count($formatArguments) !== 1) { - $name = $reflectionObject->getName(); - throw new InternalServerException("The entity $name does not have a single attribute argument."); - } - - return $formatArguments[0]; + $formatAttribute = $formatAttributes[0]->newInstance(); + return $formatAttribute->class; } /** @@ -111,28 +104,28 @@ public static function extractRequestParamData(ReflectionMethod $reflectionMetho return $data; } - public static function extractRequestAttributeData( - ReflectionClass|ReflectionProperty|ReflectionMethod $reflectionObject - ): ?RequestParamData { - $requestAttribute = $reflectionObject->getAttributes(FormatParameterAttribute::class); - if (count($requestAttribute) === 0) { + public static function extractFormatParameterData(ReflectionProperty $reflectionObject): ?RequestParamData + { + $requestAttributes = $reflectionObject->getAttributes(FormatParameterAttribute::class); + if (count($requestAttributes) === 0) { return null; } - $requestArguments = $requestAttribute[0]->getArguments(); - $name = $reflectionObject->name; - $type = $requestArguments["type"]; - $description = array_key_exists("description", $requestArguments) ? $requestArguments["description"] : ""; - $required = array_key_exists("required", $requestArguments) ? $requestArguments["required"] : true; - - return new RequestParamData($type, $name, $description, $required); + $requestAttribute = $requestAttributes[0]->newInstance(); + return new RequestParamData( + $requestAttribute->type, + $reflectionObject->name, + $requestAttribute->description, + $requestAttribute->required, + $requestAttribute->validators + ); } /** * Debug method used to extract all attribute data of a reflection object. * @param \ReflectionClass|\ReflectionProperty|\ReflectionMethod $reflectionObject The reflection object. * @return array Returns an array, where each element represents an attribute in top-down order of definition - * in the code. Each element is an array of constructor arguments of the attribute. + * in the code. Each element is an instance of the specific attribute. */ public static function debugGetAttributes( ReflectionClass|ReflectionProperty|ReflectionMethod $reflectionObject @@ -140,7 +133,7 @@ public static function debugGetAttributes( $requestAttributes = $reflectionObject->getAttributes(); $data = []; foreach ($requestAttributes as $attr) { - $data[] = $attr->getArguments(); + $data[] = $attr->newInstance(); } return $data; } @@ -164,7 +157,7 @@ public static function createNameToFieldDefinitionsMap(string $className) $fieldType = $reflectionType?->getName(); $nullable = $reflectionType?->allowsNull() ?? false; - $requestParamData = self::extractRequestAttributeData($field); + $requestParamData = self::extractFormatParameterData($field); if ($requestParamData === null) { throw new InternalServerException( "The field $fieldName of class $className does not have a RequestAttribute." @@ -210,44 +203,6 @@ public static function createFormatToClassMap() return $formatClassMap; } - /** - * Extracts all primitive validator methods (starting with "validate") and returns a map from format to a callback. - * The callbacks have one parameter that is passed to the validator. - */ - private static function getPrimitiveValidators(): array - { - $instance = new PrimitiveFormatValidators(); - $className = get_class($instance); - $methodNames = get_class_methods($className); - - $validators = []; - foreach ($methodNames as $methodName) { - // all validation methods start with validate - if (!str_starts_with($methodName, "validate")) { - continue; - } - - $annotations = AnnotationHelper::getMethodAnnotations($className, $methodName); - $format = self::extractFormatData($annotations); - $callback = function ($param) use ($instance, $methodName) { - return $instance->$methodName($param); - }; - $validators[$format] = $callback; - } - - return $validators; - } - - private static function getMetaValidators(): array - { - return []; - } - - public static function getValidators(): array - { - return array_merge(self::getPrimitiveValidators(), self::getMetaValidators()); - } - /** * Creates a MetaFormat instance of the given format. * @param string $format The name of the format. @@ -265,4 +220,15 @@ public static function createFormatInstance(string $format): MetaFormat $instance = new $className(); return $instance; } + + /** + * Checks whether a value is of a given type. + * @param mixed $value The value to be tested. + * @param \App\Helpers\MetaFormats\PhpTypes $type The desired type of the value. + * @return bool Returns whether the value is of the given type. + */ + public static function checkType($value, PhpTypes $type): bool + { + return gettype($value) === $type->value; + } } diff --git a/app/helpers/MetaFormats/PrimitiveFormatValidators.php b/app/helpers/MetaFormats/PrimitiveFormatValidators.php deleted file mode 100644 index 4bf6446cb..000000000 --- a/app/helpers/MetaFormats/PrimitiveFormatValidators.php +++ /dev/null @@ -1,39 +0,0 @@ -value; - } -} diff --git a/app/helpers/MetaFormats/Validators/StringValidator.php b/app/helpers/MetaFormats/Validators/StringValidator.php index 8d3bafb3b..62e35ea1c 100644 --- a/app/helpers/MetaFormats/Validators/StringValidator.php +++ b/app/helpers/MetaFormats/Validators/StringValidator.php @@ -2,8 +2,8 @@ namespace App\Helpers\MetaFormats\Validators; +use App\Helpers\MetaFormats\MetaFormatHelper; use App\Helpers\MetaFormats\PhpTypes; -use App\Helpers\MetaFormats\PrimitiveFormatValidators; class StringValidator { @@ -21,7 +21,7 @@ public function __construct(int $minLength = 0, int $maxLength = -1, ?string $re public function validate(string $value) { - if (!PrimitiveFormatValidators::checkType($value, PhpTypes::String)) { + if (!MetaFormatHelper::checkType($value, PhpTypes::String)) { return false; } From b79bad0e5cf70b63ddca5fd626dd88f85562a280 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 23 Jan 2025 14:23:04 +0100 Subject: [PATCH 018/103] loose attributes work now (requestFormatInstance is now created properly using a stdClass) --- app/V1Module/presenters/RegistrationPresenter.php | 2 +- app/V1Module/presenters/base/BasePresenter.php | 10 ++++++++-- app/helpers/MetaFormats/FieldFormatDefinition.php | 1 + app/helpers/MetaFormats/MetaRequest.php | 4 ++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/V1Module/presenters/RegistrationPresenter.php b/app/V1Module/presenters/RegistrationPresenter.php index bf7082be9..27aade7f9 100644 --- a/app/V1Module/presenters/RegistrationPresenter.php +++ b/app/V1Module/presenters/RegistrationPresenter.php @@ -165,7 +165,7 @@ public function checkCreateAccount() * @throws WrongCredentialsException * @throws InvalidArgumentException */ - #[FormatAttribute("userRegistration")] + #[FormatAttribute(UserFormat::class)] public function actionCreateAccount() { $req = $this->getMetaRequest(); diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index c3e41bbed..648ee4c24 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -75,8 +75,8 @@ class BasePresenter extends \App\Presenters\BasePresenter */ public $logger; - /** @var MetaFormat Processed parameters from the request */ - private MetaFormat $requestFormatInstance; + /** @var mixed Processed parameters from the request (MetaFormat or stdClass) */ + private mixed $requestFormatInstance; protected function formatPermissionCheckMethod($action) { @@ -236,9 +236,12 @@ private function processParams(ReflectionMethod $reflection) private function processParamsLoose(array $paramData) { + $formatInstanceArr = []; + // validate each param foreach ($paramData as $param) { $paramValue = $this->getValueFromParamData($param); + $formatInstanceArr[$param->name] = $paramValue; // check if null if ($paramValue === null) { @@ -257,6 +260,9 @@ private function processParamsLoose(array $paramData) } } } + + // cast to stdClass + $this->requestFormatInstance = (object)$formatInstanceArr; } private function processParamsFormat(string $format) diff --git a/app/helpers/MetaFormats/FieldFormatDefinition.php b/app/helpers/MetaFormats/FieldFormatDefinition.php index e17f08dee..2dccf6091 100644 --- a/app/helpers/MetaFormats/FieldFormatDefinition.php +++ b/app/helpers/MetaFormats/FieldFormatDefinition.php @@ -106,6 +106,7 @@ private static function recursiveFormatChecker(mixed $value, FormatParser $parse } // check whether the validator exists + ///TODO: replace this mechanism $validators = FormatCache::getValidators(); if (!array_key_exists($parsedFormat->format, $validators)) { throw new InternalServerException("The format {$parsedFormat->format} does not have a validator."); diff --git a/app/helpers/MetaFormats/MetaRequest.php b/app/helpers/MetaFormats/MetaRequest.php index af116996b..e1cf56874 100644 --- a/app/helpers/MetaFormats/MetaRequest.php +++ b/app/helpers/MetaFormats/MetaRequest.php @@ -7,9 +7,9 @@ class MetaRequest { private Request $baseRequest; - private MetaFormat $requestFormatInstance; + private mixed $requestFormatInstance; - public function __construct(Request $request, MetaFormat $requestFormatInstance) + public function __construct(Request $request, mixed $requestFormatInstance) { $this->baseRequest = $request; $this->requestFormatInstance = $requestFormatInstance; From 42934b0a401043c6c538c2c454d70ff82fdac3a2 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 23 Jan 2025 15:49:47 +0100 Subject: [PATCH 019/103] loose attributes now support nullability --- .../presenters/base/BasePresenter.php | 7 ++++ .../AnnotationToAttributeConverter.php | 32 +++++++++++++------ .../Attributes/FormatParameterAttribute.php | 5 +++ .../Attributes/RequestParamAttribute.php | 4 +++ .../FormatDefinitions/UserFormat.php | 9 ++++-- app/helpers/MetaFormats/MetaFormatHelper.php | 6 ++-- app/helpers/MetaFormats/RequestParamData.php | 6 +++- 7 files changed, 54 insertions(+), 15 deletions(-) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 648ee4c24..0585008bb 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -249,6 +249,13 @@ private function processParamsLoose(array $paramData) throw new InvalidArgumentException($param->name, "The parameter is required and cannot be null."); } + if (!$param->nullable) { + throw new InvalidArgumentException( + $param->name, + "The parameter is not nullable and thus cannot be null." + ); + } + // only non null values should be validated continue; } diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index f0e3cc0f4..16be417f1 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -49,6 +49,10 @@ private static function regexCaptureToAttributeCallback(array $matches) $parenthesesBuilder = new ParenthesesBuilder(); // add type + if (!array_key_exists("type", $annotationParameters)) { + throw new InternalServerException("Missing type parameter."); + } + $typeStr = $annotationParameters["type"]; $type = null; switch ($typeStr) { @@ -73,22 +77,37 @@ private static function regexCaptureToAttributeCallback(array $matches) $parenthesesBuilder->addValue("description: \"{$annotationParameters["description"]}\""); } + $nullable = false; if (array_key_exists("validation", $annotationParameters)) { - $validator = self::convertAnnotationValidationToValidatorString($annotationParameters["validation"]); - $parenthesesBuilder->addValue("validators: [ $validator ]"); + $validation = $annotationParameters["validation"]; + + if (self::checkValidationNullability($validation)) { + // remove the '|null' from the end of the string + $validation = substr($validation, 0, -5); + $nullable = true; + } + + // this will always produce a single validator (the annotations do not contain multiple validation fields) + $validator = self::convertAnnotationValidationToValidatorString($validation); + $parenthesesBuilder->addValue(value: "validators: [ $validator ]"); } if (array_key_exists("required", $annotationParameters)) { $parenthesesBuilder->addValue("required: " . $annotationParameters["required"]); } - if (!array_key_exists("type", $annotationParameters)) { - throw new InternalServerException("Missing type parameter."); + if ($nullable) { + $parenthesesBuilder->addValue("nullable: true"); } return "#[RequestParamAttribute{$parenthesesBuilder->toString()}]"; } + private static function checkValidationNullability(string $validation): bool + { + return str_ends_with($validation, "|null"); + } + /** * Converts annotation validation values (such as "string:1..255") to Validator construction * strings (such as "new StringValidator(1, 255)"). @@ -136,11 +155,6 @@ private static function convertAnnotationValidationToValidatorString(string $val return "new StringValidator()"; } - ///TODO: this ignores nullability - if (str_ends_with($validation, "|null")) { - $validation = substr($validation, 0, -5); - } - switch ($validation) { case "email": // there is one occurrence of this diff --git a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php index 804f4ae29..ccb78d126 100644 --- a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php +++ b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php @@ -15,22 +15,27 @@ class FormatParameterAttribute public string $description; public bool $required; public array $validators; + // there is not an easy way to check whether a property has the nullability flag set + public bool $nullable; /** * @param \App\Helpers\MetaFormats\RequestParamType $type The request parameter type (Post or Query). * @param string $description The description of the request parameter. * @param bool $required Whether the request parameter is required. * @param array $validators An array of validators applied to the request parameter. + * @param bool $nullable Whether the request parameter can be null. */ public function __construct( RequestParamType $type, string $description = "", bool $required = true, array $validators = [], + bool $nullable = false, ) { $this->type = $type; $this->description = $description; $this->required = $required; $this->validators = $validators; + $this->nullable = $nullable; } } diff --git a/app/helpers/MetaFormats/Attributes/RequestParamAttribute.php b/app/helpers/MetaFormats/Attributes/RequestParamAttribute.php index 17ae771cf..be6002caa 100644 --- a/app/helpers/MetaFormats/Attributes/RequestParamAttribute.php +++ b/app/helpers/MetaFormats/Attributes/RequestParamAttribute.php @@ -16,6 +16,7 @@ class RequestParamAttribute public string $description; public bool $required; public array $validators; + public bool $nullable; /** * @param \App\Helpers\MetaFormats\RequestParamType $type The request parameter type (Post or Query). @@ -23,6 +24,7 @@ class RequestParamAttribute * @param string $description The description of the request parameter. * @param bool $required Whether the request parameter is required. * @param array $validators An array of validators applied to the request parameter. + * @param bool $nullable Whether the request parameter can be null. */ public function __construct( RequestParamType $type, @@ -30,11 +32,13 @@ public function __construct( string $description = "", bool $required = true, array $validators = [], + bool $nullable = false, ) { $this->type = $type; $this->paramName = $name; $this->description = $description; $this->required = $required; $this->validators = $validators; + $this->nullable = $nullable; } } diff --git a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php index c417841ed..87d8226d5 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php @@ -6,6 +6,7 @@ use App\Helpers\MetaFormats\MetaFormat; use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute; use App\Helpers\MetaFormats\RequestParamType; +use App\Helpers\MetaFormats\Validators\StringValidator; #[FormatAttribute(UserFormat::class)] class UserFormat extends MetaFormat @@ -17,7 +18,7 @@ class UserFormat extends MetaFormat #[FormatParameterAttribute(type: RequestParamType::Post, description: "First name")] public string $firstName; - #[FormatParameterAttribute(type: RequestParamType::Post, description: "Last name")] + #[FormatParameterAttribute(type: RequestParamType::Post, description: "Last name", validators: [ new StringValidator(2) ])] public string $lastName; #[FormatParameterAttribute(type: RequestParamType::Post, description: "A password for authentication")] @@ -32,14 +33,16 @@ class UserFormat extends MetaFormat #[FormatParameterAttribute( type: RequestParamType::Post, description: "Titles that are placed before user name", - required: false + required: false, + nullable: true )] public ?string $titlesBeforeName; #[FormatParameterAttribute( type: RequestParamType::Post, description: "Titles that are placed after user name", - required: false + required: false, + nullable: true )] public ?string $titlesAfterName; } diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 3be083e0d..5ee6aa1e7 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -97,7 +97,8 @@ public static function extractRequestParamData(ReflectionMethod $reflectionMetho $paramAttr->paramName, $paramAttr->description, $paramAttr->required, - $paramAttr->validators + $paramAttr->validators, + $paramAttr->nullable, ); } @@ -117,7 +118,8 @@ public static function extractFormatParameterData(ReflectionProperty $reflection $reflectionObject->name, $requestAttribute->description, $requestAttribute->required, - $requestAttribute->validators + $requestAttribute->validators, + $requestAttribute->nullable, ); } diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 98b7df46b..ce1b72821 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -12,19 +12,22 @@ class RequestParamData public string $description; public bool $required; public array $validators; + public bool $nullable; public function __construct( RequestParamType $type, string $name, string $description, bool $required, - array $validators = [] + array $validators = [], + bool $nullable = false, ) { $this->type = $type; $this->name = $name; $this->description = $description; $this->required = $required; $this->validators = $validators; + $this->nullable = $nullable; } public function toAnnotationParameterData() @@ -34,6 +37,7 @@ public function toAnnotationParameterData() $dataType = $this->validators[0]::SWAGGER_TYPE; } + ///TODO: does not pass null return new AnnotationParameterData( $dataType, $this->name, From f8991d1b96d98457f192c0fe04bfee7a970f5ee5 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 23 Jan 2025 19:51:48 +0100 Subject: [PATCH 020/103] merged FieldFormatDefinition and RequestParamData - now all checks are done using Validators (except for nullability/required) --- .../presenters/base/BasePresenter.php | 31 +---- .../MetaFormats/FieldFormatDefinition.php | 117 ------------------ app/helpers/MetaFormats/MetaFormatHelper.php | 10 +- app/helpers/MetaFormats/RequestParamData.php | 39 ++++++ 4 files changed, 45 insertions(+), 152 deletions(-) delete mode 100644 app/helpers/MetaFormats/FieldFormatDefinition.php diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 0585008bb..b33cb8113 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -243,29 +243,9 @@ private function processParamsLoose(array $paramData) $paramValue = $this->getValueFromParamData($param); $formatInstanceArr[$param->name] = $paramValue; - // check if null - if ($paramValue === null) { - if ($param->required) { - throw new InvalidArgumentException($param->name, "The parameter is required and cannot be null."); - } - - if (!$param->nullable) { - throw new InvalidArgumentException( - $param->name, - "The parameter is not nullable and thus cannot be null." - ); - } - - // only non null values should be validated - continue; - } - - // use every provided validator - foreach ($param->validators as $validator) { - if (!$validator->validate($paramValue)) { - throw new InvalidArgumentException($param->name); - } - } + // this throws when it does not conform + $this->logger->log(var_export($param, true), ILogger::DEBUG); + $param->conformsToDefinition($paramValue); } // cast to stdClass @@ -285,10 +265,7 @@ private function processParamsFormat(string $format) ///TODO: handle nested MetaFormat creation $formatInstance = MetaFormatHelper::createFormatInstance($format); - foreach ($nameToFieldDefinitionsMap as $fieldName => $fieldData) { - $requestParamData = $fieldData->requestData; - //$this->logger->log(var_export($requestParamData, true), ILogger::DEBUG); - + foreach ($nameToFieldDefinitionsMap as $fieldName => $requestParamData) { $value = $this->getValueFromParamData($requestParamData); if (!$formatInstance->checkedAssign($fieldName, $value)) { diff --git a/app/helpers/MetaFormats/FieldFormatDefinition.php b/app/helpers/MetaFormats/FieldFormatDefinition.php deleted file mode 100644 index 2dccf6091..000000000 --- a/app/helpers/MetaFormats/FieldFormatDefinition.php +++ /dev/null @@ -1,117 +0,0 @@ - "bool", - "integer" => "int", - "double" => "double", - "string" => "string", - "array" => "array", - "object" => "object", - "resource" => "resource", - "NULL" => "null", - ]; - - /** - * Constructs a field format definition. - * Either the @format or @type parameter need to have a non-null value (or both). - * @param ?string $format The format of the field. - * @param ?string $type The PHP type of the field yielded by a 'ReflectionProperty::getType()' call. - * @param bool $nullable Whether the type is nullable. - * @param RequestParamData $requestData Request data such as param type and description. - * @throws \App\Exceptions\InternalServerException Thrown when both @format and @type were null. - */ - public function __construct(?string $format, ?string $type, bool $nullable, RequestParamData $requestData) - { - // if both are null, there is no way to validate an assigned value - if ($format === null && $type === null) { - throw new InternalServerException("Both the format and type of a field definition were undefined."); - } - - $this->format = $format; - $this->type = $type; - $this->nullable = $nullable; - $this->requestData = $requestData; - } - - /** - * Checks whether a value meets this definition. - * @param mixed $value The value to be checked. - * @throws \App\Exceptions\InternalServerException Thrown when the format does not have a validator. - * @return bool Returns whether the value passed the test. - */ - public function conformsToDefinition(mixed $value) - { - // use format validators if possible - if ($this->format !== null) { - // enables parsing more complicated formats (string[]?, string?[], string?[][]?, ...) - $parsedFormat = new FormatParser($this->format); - return self::recursiveFormatChecker($value, $parsedFormat); - } - - // if the value is null and is a base type, check the nullability of the base type - if ($value == null) { - return $this->nullable; - } - - // convert the gettype return value to the reflective return value - $valueType = gettype($value); - if (!array_key_exists($valueType, self::$gettypeToReflectiveMap)) { - throw new InternalServerException("Unknown gettype value: $valueType"); - } - - return $valueType === $this->type; - } - - /** - * Checks whether the value fits a format recursively. - * The format can contain array modifiers and thus all array elements need to be checked recursively. - * @param mixed $value The value to be checked - * @param \App\Helpers\MetaFormats\FormatParser $parsedFormat A parsed format used for recursive traversal. - * @throws \App\Exceptions\InternalServerException Thrown when a format does not have a validator. - * @return bool Returns whether the value conforms to the format. - */ - private static function recursiveFormatChecker(mixed $value, FormatParser $parsedFormat): bool - { - // check nullability - if ($value === null) { - return $parsedFormat->nullable; - } - - // handle arrays - if ($parsedFormat->isArray) { - if (!is_array($value)) { - return false; - } - - // if any element fails, the whole format fails - foreach ($value as $element) { - if (!self::recursiveFormatChecker($element, $parsedFormat->nested)) { - return false; - } - } - return true; - } - - // check whether the validator exists - ///TODO: replace this mechanism - $validators = FormatCache::getValidators(); - if (!array_key_exists($parsedFormat->format, $validators)) { - throw new InternalServerException("The format {$parsedFormat->format} does not have a validator."); - } - - return $validators[$parsedFormat->format]($value); - } -} diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 5ee6aa1e7..4684af767 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -152,13 +152,6 @@ public static function createNameToFieldDefinitionsMap(string $className) $formats = []; foreach ($fields as $fieldName => $value) { $field = $class->getProperty($fieldName); - // the format can be null (not present) - $format = self::extractFormatFromAttribute($field); - // get null if there is no type - $reflectionType = $field->getType(); - $fieldType = $reflectionType?->getName(); - $nullable = $reflectionType?->allowsNull() ?? false; - $requestParamData = self::extractFormatParameterData($field); if ($requestParamData === null) { throw new InternalServerException( @@ -166,7 +159,8 @@ public static function createNameToFieldDefinitionsMap(string $className) ); } - $formats[$fieldName] = new FieldFormatDefinition($format, $fieldType, $nullable, $requestParamData); + ///TODO: add base type (PHP type of the field) validators to $requestParamData + $formats[$fieldName] = $requestParamData; } return $formats; diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index ce1b72821..09e388435 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -2,6 +2,7 @@ namespace App\Helpers\MetaFormats; +use App\Exceptions\InvalidArgumentException; use App\Helpers\MetaFormats\Validators\StringValidator; use App\Helpers\Swagger\AnnotationParameterData; @@ -30,6 +31,44 @@ public function __construct( $this->nullable = $nullable; } + /** + * Checks whether a value meets this definition. + * @param mixed $value The value to be checked. + * @throws \App\Exceptions\InvalidArgumentException Thrown when the value does not meet the definition. + * @return bool Returns whether the value passed the test. + */ + public function conformsToDefinition(mixed $value) + { + // check if null + if ($value === null) { + if (!$this->required) { + ///TODO: what if a required param can be null? Does that mean that required & null is fine? How to check the required constrains then? + //throw new InvalidArgumentException($this->name, "The parameter is required and cannot be null."); + return true; + } + + if (!$this->nullable) { + throw new InvalidArgumentException( + $this->name, + "The parameter is not nullable and thus cannot be null." + ); + } + + // only non null values should be validated + // (validators do not expect null) + return true; + } + + // use every provided validator + foreach ($this->validators as $validator) { + if (!$validator->validate($value)) { + throw new InvalidArgumentException($this->name); + } + } + + return true; + } + public function toAnnotationParameterData() { $dataType = null; From 5dd69c48f0061aabfdd9c9c7684d22935cda7e4e Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 24 Jan 2025 11:00:57 +0100 Subject: [PATCH 021/103] improved swagger typing, added swagger descriptions --- app/commands/MetaTester.php | 44 ++++-- app/helpers/MetaFormats/RequestParamData.php | 30 +++- .../MetaFormats/Validators/ArrayValidator.php | 29 +++- .../MetaFormats/Validators/BoolValidator.php | 2 +- .../MetaFormats/Validators/EmailValidator.php | 9 +- .../MetaFormats/Validators/FloatValidator.php | 2 +- .../MetaFormats/Validators/IntValidator.php | 2 +- .../Validators/StringValidator.php | 7 +- .../Validators/TimestampValidator.php | 2 +- .../MetaFormats/Validators/UuidValidator.php | 7 +- app/helpers/Swagger/AnnotationData.php | 11 +- app/helpers/Swagger/AnnotationHelper.php | 134 +++++++++++++----- .../Swagger/AnnotationParameterData.php | 121 +++++++--------- 13 files changed, 269 insertions(+), 131 deletions(-) diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php index 1482ac5d3..e33da7c60 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaTester.php @@ -6,6 +6,8 @@ use App\Helpers\MetaFormats\FormatDefinitions\GroupFormat; use App\Helpers\MetaFormats\FormatDefinitions\UserFormat; use App\Helpers\MetaFormats\MetaFormatHelper; +use App\Helpers\MetaFormats\Validators\ArrayValidator; +use App\Helpers\MetaFormats\Validators\StringValidator; use App\Helpers\Swagger\AnnotationHelper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -31,21 +33,8 @@ protected function execute(InputInterface $input, OutputInterface $output) return Command::SUCCESS; } - public function test(string $arg) + public function generatePresenters() { - // $view = new TestView(); - // $view->endpoint([ - // "id" => "0", - // "organizational" => false, - // ], "0001"); - // // $view->get_user_info(0); - - // $format = new GroupFormat(); - // var_dump($format->checkIfAssignable("primaryAdminsIds", [ "10000000-2000-4000-8000-160000000000", "10000000-2000-4000-8000-160000000000" ])); - - // $format = new UserFormat(); - // var_dump($format->checkedAssign("email", "a@a.a.a")); - $inDir = __DIR__ . "/../V1Module/presenters"; $outDir = __DIR__ . "/../V1Module/presenters2"; @@ -79,9 +68,36 @@ public function test(string $arg) fwrite($newFile, $newContent); fclose($newFile); } + } + + public function test(string $arg) + { + // $view = new TestView(); + // $view->endpoint([ + // "id" => "0", + // "organizational" => false, + // ], "0001"); + // // $view->get_user_info(0); + // $format = new GroupFormat(); + // var_dump($format->checkIfAssignable("primaryAdminsIds", [ "10000000-2000-4000-8000-160000000000", "10000000-2000-4000-8000-160000000000" ])); + + // $format = new UserFormat(); + // var_dump($format->checkedAssign("email", "a@a.a.a")); + + // $reflection = AnnotationHelper::getMethod("App\V1Module\Presenters\RegistrationPresenter", "actionCreateAccount"); // $attrs = MetaFormatHelper::extractRequestParamData($reflection); // var_dump($attrs); + + // $this->generatePresenters(); + + $val = new ArrayValidator(); + + $name = get_class($val) . "::DEFAULT_SWAGGER_VALUE"; + var_dump($name); + var_dump(defined($name)); + + } } diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 09e388435..52af534b9 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -3,6 +3,7 @@ namespace App\Helpers\MetaFormats; use App\Exceptions\InvalidArgumentException; +use App\Helpers\MetaFormats\Validators\ArrayValidator; use App\Helpers\MetaFormats\Validators\StringValidator; use App\Helpers\Swagger\AnnotationParameterData; @@ -69,19 +70,38 @@ public function conformsToDefinition(mixed $value) return true; } + private function hasValidators(): bool + { + return count($this->validators) > 0; + } + public function toAnnotationParameterData() { - $dataType = null; - if (count($this->validators) > 0) { - $dataType = $this->validators[0]::SWAGGER_TYPE; + $swaggerType = "string"; + $nestedArraySwaggerType = null; + if ($this->hasValidators()) { + $swaggerType = $this->validators[0]::SWAGGER_TYPE; + if ($this->validators[0] instanceof ArrayValidator) { + $nestedArraySwaggerType = $this->validators[0]->getElementSwaggerType(); + } + } + + // retrieve the example value from the getExampleValue method if present + $exampleValue = null; + if ($this->hasValidators() && method_exists(get_class($this->validators[0]), "getExampleValue")) { + $exampleValue = $this->validators[0]->getExampleValue(); } ///TODO: does not pass null return new AnnotationParameterData( - $dataType, + $swaggerType, $this->name, $this->description, - strtolower($this->type->name) + strtolower($this->type->name), + $this->required, + $this->nullable, + $exampleValue, + $nestedArraySwaggerType, ); } } diff --git a/app/helpers/MetaFormats/Validators/ArrayValidator.php b/app/helpers/MetaFormats/Validators/ArrayValidator.php index b905fd802..53062c6d1 100644 --- a/app/helpers/MetaFormats/Validators/ArrayValidator.php +++ b/app/helpers/MetaFormats/Validators/ArrayValidator.php @@ -9,7 +9,34 @@ class ArrayValidator { public const SWAGGER_TYPE = "array"; - public function validate(string $value) + // validator used for elements + private mixed $nestedValidator; + + public function __construct(mixed $nestedValidator = null) + { + $this->nestedValidator = $nestedValidator; + } + + public function getExampleValue() + { + if ($this->nestedValidator !== null && method_exists(get_class($this->nestedValidator), "getExampleValue")) { + return $this->nestedValidator->getExampleValue(); + } + + return null; + } + + public function getElementSwaggerType() + { + // default to string for unknown element types + if ($this->nestedValidator === null) { + return null; + } + + return $this->nestedValidator::SWAGGER_TYPE; + } + + public function validate(mixed $value) { ///TODO: check if array, check content return true; diff --git a/app/helpers/MetaFormats/Validators/BoolValidator.php b/app/helpers/MetaFormats/Validators/BoolValidator.php index 298483ee4..38c10cece 100644 --- a/app/helpers/MetaFormats/Validators/BoolValidator.php +++ b/app/helpers/MetaFormats/Validators/BoolValidator.php @@ -9,7 +9,7 @@ class BoolValidator { public const SWAGGER_TYPE = "boolean"; - public function validate(string $value) + public function validate(mixed $value) { ///TODO: check if bool return true; diff --git a/app/helpers/MetaFormats/Validators/EmailValidator.php b/app/helpers/MetaFormats/Validators/EmailValidator.php index 5d1bbc023..0ad876e16 100644 --- a/app/helpers/MetaFormats/Validators/EmailValidator.php +++ b/app/helpers/MetaFormats/Validators/EmailValidator.php @@ -12,12 +12,17 @@ public function __construct() parent::__construct(1); } - public function validate(string $value) + public function getExampleValue() + { + return "name@domain.tld"; + } + + public function validate(mixed $value): bool { if (!parent::validate($value)) { return false; } - return filter_var($value, FILTER_VALIDATE_EMAIL); + return filter_var($value, FILTER_VALIDATE_EMAIL) != false; } } diff --git a/app/helpers/MetaFormats/Validators/FloatValidator.php b/app/helpers/MetaFormats/Validators/FloatValidator.php index 5eb8682f2..e79901874 100644 --- a/app/helpers/MetaFormats/Validators/FloatValidator.php +++ b/app/helpers/MetaFormats/Validators/FloatValidator.php @@ -9,7 +9,7 @@ class FloatValidator { public const SWAGGER_TYPE = "number"; - public function validate(string $value) + public function validate(mixed $value) { ///TODO: check if float return true; diff --git a/app/helpers/MetaFormats/Validators/IntValidator.php b/app/helpers/MetaFormats/Validators/IntValidator.php index 252227281..caebf761a 100644 --- a/app/helpers/MetaFormats/Validators/IntValidator.php +++ b/app/helpers/MetaFormats/Validators/IntValidator.php @@ -9,7 +9,7 @@ class IntValidator { public const SWAGGER_TYPE = "integer"; - public function validate(string $value) + public function validate(mixed $value) { ///TODO: check if int return true; diff --git a/app/helpers/MetaFormats/Validators/StringValidator.php b/app/helpers/MetaFormats/Validators/StringValidator.php index 62e35ea1c..27b2e5fd7 100644 --- a/app/helpers/MetaFormats/Validators/StringValidator.php +++ b/app/helpers/MetaFormats/Validators/StringValidator.php @@ -19,7 +19,12 @@ public function __construct(int $minLength = 0, int $maxLength = -1, ?string $re $this->regex = $regex; } - public function validate(string $value) + public function getExampleValue() + { + return "text"; + } + + public function validate(mixed $value): bool { if (!MetaFormatHelper::checkType($value, PhpTypes::String)) { return false; diff --git a/app/helpers/MetaFormats/Validators/TimestampValidator.php b/app/helpers/MetaFormats/Validators/TimestampValidator.php index 488395f65..a56817808 100644 --- a/app/helpers/MetaFormats/Validators/TimestampValidator.php +++ b/app/helpers/MetaFormats/Validators/TimestampValidator.php @@ -9,7 +9,7 @@ class TimestampValidator { public const SWAGGER_TYPE = "string"; - public function validate(string $value) + public function validate(mixed $value): bool { ///TODO: check if timestamp return true; diff --git a/app/helpers/MetaFormats/Validators/UuidValidator.php b/app/helpers/MetaFormats/Validators/UuidValidator.php index 64df122fc..aff087dfe 100644 --- a/app/helpers/MetaFormats/Validators/UuidValidator.php +++ b/app/helpers/MetaFormats/Validators/UuidValidator.php @@ -12,7 +12,12 @@ 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 validate(string $value) + public function getExampleValue() + { + return "10000000-2000-4000-8000-160000000000"; + } + + public function validate(mixed $value): bool { return parent::validate($value); } diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index 977a5a1fa..154b79f52 100644 --- a/app/helpers/Swagger/AnnotationData.php +++ b/app/helpers/Swagger/AnnotationData.php @@ -12,17 +12,20 @@ class AnnotationData public array $pathParams; public array $queryParams; public array $bodyParams; + public ?string $endpointDescription; public function __construct( HttpMethods $httpMethod, array $pathParams, array $queryParams, - array $bodyParams + array $bodyParams, + string $endpointDescription = null, ) { $this->httpMethod = $httpMethod; $this->pathParams = $pathParams; $this->queryParams = $queryParams; $this->bodyParams = $bodyParams; + $this->endpointDescription = $endpointDescription; } /** @@ -71,6 +74,12 @@ public function toSwaggerAnnotations(string $route) $body = new ParenthesesBuilder(); $body->addKeyValue("path", $route); + // add the endpoint description when provided + if ($this->endpointDescription !== null) { + $body->addKeyValue("summary", $this->endpointDescription); + $body->addKeyValue("description", $this->endpointDescription); + } + foreach ($this->pathParams as $pathParam) { $body->addValue($pathParam->toParameterAnnotation()); } diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index f530728bf..0beaba432 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -12,6 +12,31 @@ */ class AnnotationHelper { + private static $nullableSuffix = '|null'; + private static $typeMap = [ + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'array' => 'array', + 'int' => 'integer', + 'integer' => 'integer', + 'float' => 'number', + 'number' => 'number', + 'numeric' => 'number', + 'numericint' => 'integer', + 'timestamp' => 'integer', + 'string' => 'string', + 'unicode' => 'string', + 'email' => 'string', + 'url' => 'string', + 'uri' => 'string', + 'pattern' => null, + 'alnum' => 'string', + 'alpha' => 'string', + 'digit' => 'string', + 'lower' => 'string', + 'upper' => 'string', + ]; + /** * Returns a ReflectionMethod object matching the name of the method and containing class. * @param string $className The name of the containing class. @@ -50,6 +75,45 @@ private static function extractAnnotationHttpMethod(array $annotations): HttpMet return null; } + private static function isDatatypeNullable(string $annotationType): bool + { + // if the dataType is not specified (it is null), it means that the annotation is not + // complete and defaults to a non nullable string + if ($annotationType === null) { + return false; + } + + // assumes that the typename ends with '|null' + if (str_ends_with($annotationType, self::$nullableSuffix)) { + return true; + } + + return false; + } + + /** + * Returns the swagger type associated with the annotation data type. + * @return string Returns the name of the swagger type. + */ + private static function getSwaggerType(string $annotationType): string + { + // if the type is not specified, default to a string + $type = 'string'; + $typename = $annotationType; + if ($typename !== null) { + if (self::isDatatypeNullable($annotationType)) { + $typename = substr($typename, 0, -strlen(self::$nullableSuffix)); + } + + if (self::$typeMap[$typename] === null) { + throw new \InvalidArgumentException("Error in getSwaggerType: Unknown typename: {$typename}"); + } + + $type = self::$typeMap[$typename]; + } + return $type; + } + /** * Extracts standart doc comments from endpoints, such as '@param string $id An identifier'. * Based on the HTTP route of the endpoint, the extracted param can be identified as either a path or @@ -68,18 +132,35 @@ private static function extractStandardAnnotationParams(array $annotations, stri if (str_starts_with($annotation, "@param")) { // sample: @param string $id Identifier of the user $tokens = explode(" ", $annotation); - $type = $tokens[1]; + $annotationType = $tokens[1]; // assumed that all names start with $ $name = substr($tokens[2], 1); $description = implode(" ", array_slice($tokens, 3)); + // path params have to be required + $isPathParam = false; // figure out where the parameter is located $location = 'query'; if (in_array($name, $routeParams)) { $location = 'path'; + $isPathParam = true; } - $descriptor = new AnnotationParameterData($type, $name, $description, $location); + $swaggerType = self::getSwaggerType($annotationType); + $nullable = self::isDatatypeNullable($annotationType); + + ///TODO: how to find out the correct query type? + $nestedArraySwaggerType = null; + + $descriptor = new AnnotationParameterData( + $swaggerType, + $name, + $description, + $location, + $isPathParam, + $nullable, + nestedArraySwaggerType: $nestedArraySwaggerType, + ); $params[] = $descriptor; } } @@ -105,36 +186,6 @@ private static function stringArrayToAssociativeArray(array $expressions): array return $dict; } - /** - * Extracts annotation parameter data from Nette annotations starting with the '@Param' prefix. - * @param array $annotations An array of annotations. - * @return array Returns an array of AnnotationParameterData objects describing the parameters. - */ - private static function extractNetteAnnotationParams(array $annotations): array - { - $bodyParams = []; - $prefix = "@Param"; - foreach ($annotations as $annotation) { - // assumed that all body parameters have a @Param annotation - if (str_starts_with($annotation, $prefix)) { - // sample: @Param(type="post", name="uiData", validation="array|null", - // description="Structured user-specific UI data") - // remove '@Param(' from the start and ')' from the end - $body = substr($annotation, strlen($prefix) + 1, -1); - $tokens = explode(", ", $body); - $values = self::stringArrayToAssociativeArray($tokens); - $descriptor = new AnnotationParameterData( - $values["validation"], - $values["name"], - $values["description"], - $values["type"] - ); - $bodyParams[] = $descriptor; - } - } - return $bodyParams; - } - /** * Parses an annotation string and returns the lines as an array. * Lines not starting with '@' are assumed to be continuations of a parent line starting with @ (or the initial @@ -163,7 +214,8 @@ public static function getAnnotationLines(string $annotations): array $line = $lines[$i]; // skip lines not starting with '@' - if ($line[0] !== "@") { + // also do not skip the first description line + if ($i != 0 && $line[0] !== "@") { continue; } @@ -205,6 +257,19 @@ private static function getRoutePathParamNames(string $route): array return $out[1]; } + /** + * Extracts the annotation description line. + * @param array $annotations The array of annotations. + */ + private static function extractAnnotationDescription(array $annotations): ?string + { + // it is either the first line (already merged if multiline), or none at all + if (!str_starts_with($annotations[0], "@")) { + return $annotations[0]; + } + return null; + } + /** * Extracts the annotation data of an endpoint. The data contains request parameters based on their type * and the HTTP method. @@ -243,8 +308,9 @@ public static function extractAnnotationData(string $className, string $methodNa } } + $description = self::extractAnnotationDescription($methodAnnotations); - $data = new AnnotationData($httpMethod, $pathParams, $queryParams, $bodyParams); + $data = new AnnotationData($httpMethod, $pathParams, $queryParams, $bodyParams, $description); return $data; } diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index 50a198502..633fd076b 100644 --- a/app/helpers/Swagger/AnnotationParameterData.php +++ b/app/helpers/Swagger/AnnotationParameterData.php @@ -2,93 +2,59 @@ namespace App\Helpers\Swagger; +use App\Exceptions\InternalServerException; + /** * Contains data of a single annotation parameter. */ class AnnotationParameterData { - public string | null $dataType; + public string $swaggerType; public string $name; - public string | null $description; + public ?string $description; public string $location; - - private static $nullableSuffix = '|null'; - private static $typeMap = [ - 'bool' => 'boolean', - 'boolean' => 'boolean', - 'array' => 'array', - 'int' => 'integer', - 'integer' => 'integer', - 'float' => 'number', - 'number' => 'number', - 'numeric' => 'number', - 'numericint' => 'integer', - 'timestamp' => 'integer', - 'string' => 'string', - 'unicode' => 'string', - 'email' => 'string', - 'url' => 'string', - 'uri' => 'string', - 'pattern' => null, - 'alnum' => 'string', - 'alpha' => 'string', - 'digit' => 'string', - 'lower' => 'string', - 'upper' => 'string', - ]; + public bool $required; + public bool $nullable; + public ?string $example; + public ?string $nestedArraySwaggerType; public function __construct( - string | null $dataType, + string $swaggerType, string $name, - string | null $description, - string $location + ?string $description, + string $location, + bool $required, + bool $nullable, + string $example = null, + string $nestedArraySwaggerType = null, ) { - $this->dataType = $dataType; + $this->swaggerType = $swaggerType; $this->name = $name; $this->description = $description; $this->location = $location; + $this->required = $required; + $this->nullable = $nullable; + $this->example = $example; + $this->nestedArraySwaggerType = $nestedArraySwaggerType; } - private function isDatatypeNullable(): bool + private function addArrayItemsIfArray(string $swaggerType, ParenthesesBuilder $container) { - // if the dataType is not specified (it is null), it means that the annotation is not - // complete and defaults to a non nullable string - if ($this->dataType === null) { - return false; - } + if ($swaggerType === "array") { + $itemsHead = "@OA\\Items"; + $items = new ParenthesesBuilder(); - // assumes that the typename ends with '|null' - if (str_ends_with($this->dataType, self::$nullableSuffix)) { - return true; - } - - return false; - } - - /** - * Returns the swagger type associated with the annotation data type. - * @return string Returns the name of the swagger type. - */ - private function getSwaggerType(): string - { - // if the type is not specified, default to a string - $type = 'string'; - $typename = $this->dataType; - if ($typename !== null) { - if ($this->isDatatypeNullable()) { - $typename = substr($typename, 0, -strlen(self::$nullableSuffix)); + if ($this->nestedArraySwaggerType !== null) { + $items->addKeyValue("type", $this->nestedArraySwaggerType); } - if (self::$typeMap[$typename] === null) { - ///TODO: Return the commented exception below once the meta-view formats are implemented. - /// This detaults to strings because custom types like 'email' are not supported yet. - return 'string'; + // add example value + if ($this->example != null) { + $items->addKeyValue("example", $this->example); } - //throw new \InvalidArgumentException("Error in getSwaggerType: Unknown typename: {$typename}"); - $type = self::$typeMap[$typename]; + $container->addValue($itemsHead . $items->toString()); } - return $type; } /** @@ -100,7 +66,9 @@ private function generateSchemaAnnotation(): string $head = "@OA\\Schema"; $body = new ParenthesesBuilder(); - $body->addKeyValue("type", $this->getSwaggerType()); + $body->addKeyValue("type", $this->swaggerType); + $this->addArrayItemsIfArray($this->swaggerType, $body); + return $head . $body->toString(); } @@ -111,10 +79,11 @@ public function toParameterAnnotation(): string { $head = "@OA\\Parameter"; $body = new ParenthesesBuilder(); - + $body->addKeyValue("name", $this->name); $body->addKeyValue("in", $this->location); - $body->addKeyValue("required", !$this->isDatatypeNullable()); + $body->addKeyValue("required", $this->required); + if ($this->description !== null) { $body->addKeyValue("description", $this->description); } @@ -135,7 +104,23 @@ public function toPropertyAnnotation(): string ///TODO: Once the meta-view formats are implemented, add support for property nullability here. $body->addKeyValue("property", $this->name); - $body->addKeyValue("type", $this->getSwaggerType()); + $body->addKeyValue("type", $this->swaggerType); + $body->addKeyValue("nullable", $this->nullable); + + if ($this->description !== null) { + $body->addKeyValue("description", $this->description); + } + + // handle arrays + $this->addArrayItemsIfArray($this->swaggerType, $body); + + // add example value + if ($this->swaggerType !== "array") { + if ($this->example != null) { + $body->addKeyValue("example", $this->example); + } + } + return $head . $body->toString(); } } From 8763ca0cd807bb0288c6f8644a336a57c69984ab Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 24 Jan 2025 11:24:56 +0100 Subject: [PATCH 022/103] unannotated path params are now handled properly --- app/helpers/Swagger/AnnotationHelper.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index 0beaba432..98495842e 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -126,6 +126,8 @@ private static function extractStandardAnnotationParams(array $annotations, stri { $routeParams = self::getRoutePathParamNames($route); + ///TODO: there can be unannotated query params as well + $params = []; foreach ($annotations as $annotation) { // assumed that all query parameters have a @param annotation @@ -144,6 +146,9 @@ private static function extractStandardAnnotationParams(array $annotations, stri if (in_array($name, $routeParams)) { $location = 'path'; $isPathParam = true; + // remove the path param from the path param list to detect parameters left behind + // (this happens when the path param does not have an annotation line) + unset($routeParams[array_search($name, $routeParams)]); } $swaggerType = self::getSwaggerType($annotationType); @@ -164,6 +169,21 @@ private static function extractStandardAnnotationParams(array $annotations, stri $params[] = $descriptor; } } + + // handle path params without annotations + foreach ($routeParams as $pathParam) { + $descriptor = new AnnotationParameterData( + // some type needs to be assigned and string seems reasonable for a param without any info + "string", + $pathParam, + null, + "path", + true, + false, + ); + $params[] = $descriptor; + } + return $params; } From fb56a6021ba8c10397807e3d0391aa2180f27c15 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 29 Jan 2025 14:10:12 +0100 Subject: [PATCH 023/103] the annotation to attribute converter no longer uses hardcoded string classnames --- .../AnnotationToAttributeConverter.php | 106 +++++++++++++----- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index 16be417f1..ef8342ae7 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -3,6 +3,15 @@ namespace App\Helpers\MetaFormats; use App\Exceptions\InternalServerException; +use App\Helpers\MetaFormats\Attributes\RequestParamAttribute; +use App\Helpers\MetaFormats\Validators\ArrayValidator; +use App\Helpers\MetaFormats\Validators\BoolValidator; +use App\Helpers\MetaFormats\Validators\EmailValidator; +use App\Helpers\MetaFormats\Validators\FloatValidator; +use App\Helpers\MetaFormats\Validators\IntValidator; +use App\Helpers\MetaFormats\Validators\StringValidator; +use App\Helpers\MetaFormats\Validators\TimestampValidator; +use App\Helpers\MetaFormats\Validators\UuidValidator; use App\Helpers\Swagger\ParenthesesBuilder; class AnnotationToAttributeConverter @@ -17,6 +26,12 @@ class AnnotationToAttributeConverter */ private static string $postRegex = "/\*\s*@Param\((?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?([a-z]+?=.+)\)/"; + private static function shortenClass(string $className) + { + $tokens = explode("\\", $className); + return end($tokens); + } + /** * Converts an array of preg_replace_callback matches to an attribute string. * @param array $matches An array of matches, with empty captures as NULL (PREG_UNMATCHED_AS_NULL flag). @@ -54,13 +69,14 @@ private static function regexCaptureToAttributeCallback(array $matches) } $typeStr = $annotationParameters["type"]; + $paramTypeClass = self::shortenClass(RequestParamType::class); $type = null; switch ($typeStr) { case "post": - $type = "RequestParamType::Post"; + $type = $paramTypeClass . "::Post"; break; case "query": - $type = "RequestParamType::Query"; + $type = $paramTypeClass . "::Query"; break; default: throw new InternalServerException("Unknown request type: $typeStr"); @@ -100,7 +116,8 @@ private static function regexCaptureToAttributeCallback(array $matches) $parenthesesBuilder->addValue("nullable: true"); } - return "#[RequestParamAttribute{$parenthesesBuilder->toString()}]"; + $paramAttributeClass = self::shortenClass(RequestParamAttribute::class); + return "#[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; } private static function checkValidationNullability(string $validation): bool @@ -117,6 +134,8 @@ private static function checkValidationNullability(string $validation): bool private static function convertAnnotationValidationToValidatorString(string $validation): string { if (str_starts_with($validation, "string")) { + $stringValidator = self::shortenClass(StringValidator::class); + // handle string length constraints, such as "string:1..255" if (strlen($validation) > 6) { if ($validation[6] !== ":") { @@ -126,7 +145,7 @@ private static function convertAnnotationValidationToValidatorString(string $val // special case for uuids if ($suffix === "36") { - return "new UuidValidator()"; + return "new " . self::shortenClass(UuidValidator::class) . "()"; } // capture the two bounding numbers and the double dot in strings of @@ -137,46 +156,74 @@ private static function convertAnnotationValidationToValidatorString(string $val // type "255", exact match if ($matches[2] === null) { - return "new StringValidator({$matches[1]}, {$matches[1]})"; + return "new {$stringValidator}({$matches[1]}, {$matches[1]})"; // type "1..255" } elseif ($matches[1] !== null && $matches[3] !== null) { - return "new StringValidator({$matches[1]}, {$matches[3]})"; + return "new {$stringValidator}({$matches[1]}, {$matches[3]})"; // type "..255" } elseif ($matches[1] === null) { - return "new StringValidator(0, {$matches[3]})"; + return "new {$stringValidator}(0, {$matches[3]})"; // type "1.." } elseif ($matches[3] === null) { - return "new StringValidator({$matches[1]})"; + return "new {$stringValidator}({$matches[1]})"; } throw new InternalServerException("Unknown string validation format: $validation"); } - return "new StringValidator()"; + return "new {$stringValidator}()"; } + // non-string validation rules do not have parameters, so they can be converted directly + $validatorClass = null; switch ($validation) { case "email": // there is one occurrence of this case "email:1..": - return "new EmailValidator()"; + $validatorClass = EmailValidator::class; + break; case "numericint": - return "new IntValidator()"; + $validatorClass = IntValidator::class; + break; case "bool": case "boolean": - return "new BoolValidator()"; + $validatorClass = BoolValidator::class; + break; case "array": case "list": - return "new ArrayValidator()"; + $validatorClass = ArrayValidator::class; + break; case "timestamp": - return "new TimestampValidator()"; + $validatorClass = TimestampValidator::class; + break; case "numeric": - return "new FloatValidator()"; + $validatorClass = FloatValidator::class; + break; default: - ///TODO - var_dump("unsupported: $validation"); - return "\"UNSUPPORTED\""; + throw new InternalServerException("Unknown validation rule: $validation"); } + + return "new " . self::shortenClass($validatorClass) . "()"; + } + + /** + * @return string[] Returns an array of Validator class names (without the namespace). + */ + private static function getValidatorNames() + { + $dir = __DIR__ . "/Validators"; + $baseFilenames = scandir($dir); + $classNames = []; + foreach ($baseFilenames as $filename) { + if (!str_ends_with($filename, ".php")) { + continue; + } + + // remove the ".php" suffix + $className = substr($filename, 0, -4); + $classNames[] = $className; + } + return $classNames; } public static function convertFile(string $path) @@ -191,25 +238,22 @@ public static function convertFile(string $path) $lines = []; $attributeLinesBuffer = []; $usingsAdded = false; + $paramAttributeClass = self::shortenClass(RequestParamAttribute::class); foreach (preg_split("/((\r?\n)|(\r\n?))/", $withInterleavedAttributes) as $line) { - // add usings for new types + // detected the initial "use" block, add usings for new types if (!$usingsAdded && strlen($line) > 3 && substr($line, 0, 3) === "use") { - $lines[] = "use App\Helpers\MetaFormats\Attributes\RequestParamAttribute;"; + $lines[] = "use App\Helpers\MetaFormats\Attributes\{$paramAttributeClass};"; $lines[] = "use App\Helpers\MetaFormats\RequestParamType;"; - $lines[] = "use App\Helpers\MetaFormats\Validators\StringValidator;"; - $lines[] = "use App\Helpers\MetaFormats\Validators\EmailValidator;"; - $lines[] = "use App\Helpers\MetaFormats\Validators\UuidValidator;"; - $lines[] = "use App\Helpers\MetaFormats\Validators\BoolValidator;"; - $lines[] = "use App\Helpers\MetaFormats\Validators\ArrayValidator;"; - $lines[] = "use App\Helpers\MetaFormats\Validators\FloatValidator;"; - $lines[] = "use App\Helpers\MetaFormats\Validators\IntValidator;"; - $lines[] = "use App\Helpers\MetaFormats\Validators\TimestampValidator;"; + foreach (self::getValidatorNames() as $validator) { + $lines[] = "use App\Helpers\MetaFormats\Validators\{$validator};"; + } + // write the detected line (the first detected "use" line) $lines[] = $line; $usingsAdded = true; - // store attribute lines in the buffer and do not write them - } elseif (preg_match("/#\[RequestParamAttribute/", $line) === 1) { + // detected the new attribute line, store it in the buffer and do not write it yet + } elseif (preg_match("/#\[{$paramAttributeClass}/", $line) === 1) { $attributeLinesBuffer[] = $line; - // flush attribute lines + // detected the end of the comment block "*/", flush attribute lines } elseif (trim($line) === "*/") { $lines[] = $line; foreach ($attributeLinesBuffer as $attributeLine) { From 7ebda6d0dc5285b9a15cdba433e9dedb30e9b64d Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 29 Jan 2025 14:14:12 +0100 Subject: [PATCH 024/103] replaced single backslashes with double in namespace paths --- app/helpers/MetaFormats/AnnotationToAttributeConverter.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index ef8342ae7..b59e52ea2 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -239,13 +239,14 @@ public static function convertFile(string $path) $attributeLinesBuffer = []; $usingsAdded = false; $paramAttributeClass = self::shortenClass(RequestParamAttribute::class); + $paramTypeClass = self::shortenClass(RequestParamType::class); foreach (preg_split("/((\r?\n)|(\r\n?))/", $withInterleavedAttributes) as $line) { // detected the initial "use" block, add usings for new types if (!$usingsAdded && strlen($line) > 3 && substr($line, 0, 3) === "use") { - $lines[] = "use App\Helpers\MetaFormats\Attributes\{$paramAttributeClass};"; - $lines[] = "use App\Helpers\MetaFormats\RequestParamType;"; + $lines[] = "use App\\Helpers\\MetaFormats\\Attributes\\{$paramAttributeClass};"; + $lines[] = "use App\\Helpers\\MetaFormats\\{$paramTypeClass};"; foreach (self::getValidatorNames() as $validator) { - $lines[] = "use App\Helpers\MetaFormats\Validators\{$validator};"; + $lines[] = "use App\\Helpers\\MetaFormats\\Validators\\{$validator};"; } // write the detected line (the first detected "use" line) $lines[] = $line; From c2c16b5b70dc18700c0b1641223f1fa2f78898b2 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Wed, 29 Jan 2025 15:10:02 +0100 Subject: [PATCH 025/103] renamed various classes used in parameter attributes to make them shorter, changed param attribute argument order --- .../presenters/base/BasePresenter.php | 6 +-- app/commands/MetaTester.php | 17 +++--- .../AnnotationToAttributeConverter.php | 54 +++++++++---------- .../Attributes/FormatParameterAttribute.php | 16 +++--- .../{RequestParamAttribute.php => Param.php} | 18 +++---- .../FormatDefinitions/UserFormat.php | 20 +++---- app/helpers/MetaFormats/MetaFormatHelper.php | 4 +- app/helpers/MetaFormats/RequestParamData.php | 10 ++-- .../{RequestParamType.php => Type.php} | 2 +- .../{ArrayValidator.php => VArray.php} | 2 +- .../{BoolValidator.php => VBool.php} | 2 +- .../{EmailValidator.php => VEmail.php} | 2 +- .../{FloatValidator.php => VFloat.php} | 2 +- .../Validators/{IntValidator.php => VInt.php} | 2 +- .../{StringValidator.php => VString.php} | 2 +- ...{TimestampValidator.php => VTimestamp.php} | 2 +- .../{UuidValidator.php => VUuid.php} | 2 +- 17 files changed, 81 insertions(+), 82 deletions(-) rename app/helpers/MetaFormats/Attributes/{RequestParamAttribute.php => Param.php} (80%) rename app/helpers/MetaFormats/{RequestParamType.php => Type.php} (89%) rename app/helpers/MetaFormats/Validators/{ArrayValidator.php => VArray.php} (97%) rename app/helpers/MetaFormats/Validators/{BoolValidator.php => VBool.php} (93%) rename app/helpers/MetaFormats/Validators/{EmailValidator.php => VEmail.php} (92%) rename app/helpers/MetaFormats/Validators/{FloatValidator.php => VFloat.php} (93%) rename app/helpers/MetaFormats/Validators/{IntValidator.php => VInt.php} (94%) rename app/helpers/MetaFormats/Validators/{StringValidator.php => VString.php} (97%) rename app/helpers/MetaFormats/Validators/{TimestampValidator.php => VTimestamp.php} (92%) rename app/helpers/MetaFormats/Validators/{UuidValidator.php => VUuid.php} (92%) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index b33cb8113..8f169978b 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -25,7 +25,7 @@ use App\Helpers\MetaFormats\MetaFormat; use App\Helpers\MetaFormats\MetaRequest; use App\Helpers\MetaFormats\RequestParamData; -use App\Helpers\MetaFormats\RequestParamType; +use App\Helpers\MetaFormats\Type; use App\Responses\StorageFileResponse; use App\Responses\ZipFilesResponse; use Nette\Application\Application; @@ -291,9 +291,9 @@ private function processParamsFormat(string $format) private function getValueFromParamData(RequestParamData $paramData): mixed { switch ($paramData->type) { - case RequestParamType::Post: + case Type::Post: return $this->getPostField($paramData->name, required: $paramData->required); - case RequestParamType::Query: + case Type::Query: return $this->getQueryField($paramData->name, required: $paramData->required); default: throw new InternalServerException("Unknown parameter type: {$paramData->type->name}"); diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php index e33da7c60..f04dc116c 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaTester.php @@ -3,11 +3,12 @@ namespace App\Console; use App\Helpers\MetaFormats\AnnotationToAttributeConverter; +use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute; use App\Helpers\MetaFormats\FormatDefinitions\GroupFormat; use App\Helpers\MetaFormats\FormatDefinitions\UserFormat; use App\Helpers\MetaFormats\MetaFormatHelper; -use App\Helpers\MetaFormats\Validators\ArrayValidator; -use App\Helpers\MetaFormats\Validators\StringValidator; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\Swagger\AnnotationHelper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -90,14 +91,12 @@ public function test(string $arg) // $attrs = MetaFormatHelper::extractRequestParamData($reflection); // var_dump($attrs); - // $this->generatePresenters(); - - $val = new ArrayValidator(); - - $name = get_class($val) . "::DEFAULT_SWAGGER_VALUE"; - var_dump($name); - var_dump(defined($name)); + $this->generatePresenters(); + // $val = new VArray(); + // $name = get_class($val) . "::DEFAULT_SWAGGER_VALUE"; + // var_dump($name); + // var_dump(defined($name)); } } diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index b59e52ea2..266174ced 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -3,15 +3,15 @@ namespace App\Helpers\MetaFormats; use App\Exceptions\InternalServerException; -use App\Helpers\MetaFormats\Attributes\RequestParamAttribute; -use App\Helpers\MetaFormats\Validators\ArrayValidator; -use App\Helpers\MetaFormats\Validators\BoolValidator; -use App\Helpers\MetaFormats\Validators\EmailValidator; -use App\Helpers\MetaFormats\Validators\FloatValidator; -use App\Helpers\MetaFormats\Validators\IntValidator; -use App\Helpers\MetaFormats\Validators\StringValidator; -use App\Helpers\MetaFormats\Validators\TimestampValidator; -use App\Helpers\MetaFormats\Validators\UuidValidator; +use App\Helpers\MetaFormats\Attributes\Param; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Helpers\Swagger\ParenthesesBuilder; class AnnotationToAttributeConverter @@ -69,7 +69,7 @@ private static function regexCaptureToAttributeCallback(array $matches) } $typeStr = $annotationParameters["type"]; - $paramTypeClass = self::shortenClass(RequestParamType::class); + $paramTypeClass = self::shortenClass(Type::class); $type = null; switch ($typeStr) { case "post": @@ -89,10 +89,6 @@ private static function regexCaptureToAttributeCallback(array $matches) } $parenthesesBuilder->addValue("\"{$annotationParameters["name"]}\""); - if (array_key_exists("description", $annotationParameters)) { - $parenthesesBuilder->addValue("description: \"{$annotationParameters["description"]}\""); - } - $nullable = false; if (array_key_exists("validation", $annotationParameters)) { $validation = $annotationParameters["validation"]; @@ -105,7 +101,11 @@ private static function regexCaptureToAttributeCallback(array $matches) // this will always produce a single validator (the annotations do not contain multiple validation fields) $validator = self::convertAnnotationValidationToValidatorString($validation); - $parenthesesBuilder->addValue(value: "validators: [ $validator ]"); + $parenthesesBuilder->addValue(value: "[ $validator ]"); + } + + if (array_key_exists("description", $annotationParameters)) { + $parenthesesBuilder->addValue("\"{$annotationParameters["description"]}\""); } if (array_key_exists("required", $annotationParameters)) { @@ -116,7 +116,7 @@ private static function regexCaptureToAttributeCallback(array $matches) $parenthesesBuilder->addValue("nullable: true"); } - $paramAttributeClass = self::shortenClass(RequestParamAttribute::class); + $paramAttributeClass = self::shortenClass(Param::class); return "#[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; } @@ -127,14 +127,14 @@ private static function checkValidationNullability(string $validation): bool /** * Converts annotation validation values (such as "string:1..255") to Validator construction - * strings (such as "new StringValidator(1, 255)"). + * strings (such as "new VString(1, 255)"). * @param string $validation The annotation validation string. * @return string Returns the object construction string. */ private static function convertAnnotationValidationToValidatorString(string $validation): string { if (str_starts_with($validation, "string")) { - $stringValidator = self::shortenClass(StringValidator::class); + $stringValidator = self::shortenClass(VString::class); // handle string length constraints, such as "string:1..255" if (strlen($validation) > 6) { @@ -145,7 +145,7 @@ private static function convertAnnotationValidationToValidatorString(string $val // special case for uuids if ($suffix === "36") { - return "new " . self::shortenClass(UuidValidator::class) . "()"; + return "new " . self::shortenClass(VUuid::class) . "()"; } // capture the two bounding numbers and the double dot in strings of @@ -180,24 +180,24 @@ private static function convertAnnotationValidationToValidatorString(string $val case "email": // there is one occurrence of this case "email:1..": - $validatorClass = EmailValidator::class; + $validatorClass = VEmail::class; break; case "numericint": - $validatorClass = IntValidator::class; + $validatorClass = VInt::class; break; case "bool": case "boolean": - $validatorClass = BoolValidator::class; + $validatorClass = VBool::class; break; case "array": case "list": - $validatorClass = ArrayValidator::class; + $validatorClass = VArray::class; break; case "timestamp": - $validatorClass = TimestampValidator::class; + $validatorClass = VTimestamp::class; break; case "numeric": - $validatorClass = FloatValidator::class; + $validatorClass = VFloat::class; break; default: throw new InternalServerException("Unknown validation rule: $validation"); @@ -238,8 +238,8 @@ public static function convertFile(string $path) $lines = []; $attributeLinesBuffer = []; $usingsAdded = false; - $paramAttributeClass = self::shortenClass(RequestParamAttribute::class); - $paramTypeClass = self::shortenClass(RequestParamType::class); + $paramAttributeClass = self::shortenClass(Param::class); + $paramTypeClass = self::shortenClass(Type::class); foreach (preg_split("/((\r?\n)|(\r\n?))/", $withInterleavedAttributes) as $line) { // detected the initial "use" block, add usings for new types if (!$usingsAdded && strlen($line) > 3 && substr($line, 0, 3) === "use") { diff --git a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php index ccb78d126..271ebc27c 100644 --- a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php +++ b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php @@ -2,7 +2,7 @@ namespace App\Helpers\MetaFormats\Attributes; -use App\Helpers\MetaFormats\RequestParamType; +use App\Helpers\MetaFormats\Type; use Attribute; /** @@ -11,31 +11,31 @@ #[Attribute] class FormatParameterAttribute { - public RequestParamType $type; + public Type $type; + public array $validators; public string $description; public bool $required; - public array $validators; // there is not an easy way to check whether a property has the nullability flag set public bool $nullable; /** - * @param \App\Helpers\MetaFormats\RequestParamType $type The request parameter type (Post or Query). + * @param \App\Helpers\MetaFormats\Type $type The request parameter type (Post or Query). + * @param array $validators An array of validators applied to the request parameter. * @param string $description The description of the request parameter. * @param bool $required Whether the request parameter is required. - * @param array $validators An array of validators applied to the request parameter. * @param bool $nullable Whether the request parameter can be null. */ public function __construct( - RequestParamType $type, + Type $type, + array $validators = [], string $description = "", bool $required = true, - array $validators = [], bool $nullable = false, ) { $this->type = $type; + $this->validators = $validators; $this->description = $description; $this->required = $required; - $this->validators = $validators; $this->nullable = $nullable; } } diff --git a/app/helpers/MetaFormats/Attributes/RequestParamAttribute.php b/app/helpers/MetaFormats/Attributes/Param.php similarity index 80% rename from app/helpers/MetaFormats/Attributes/RequestParamAttribute.php rename to app/helpers/MetaFormats/Attributes/Param.php index be6002caa..1e926bcd8 100644 --- a/app/helpers/MetaFormats/Attributes/RequestParamAttribute.php +++ b/app/helpers/MetaFormats/Attributes/Param.php @@ -2,43 +2,43 @@ namespace App\Helpers\MetaFormats\Attributes; -use App\Helpers\MetaFormats\RequestParamType; +use App\Helpers\MetaFormats\Type; use Attribute; /** * Attribute used to annotate individual post or query parameters of endpoints. */ #[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)] -class RequestParamAttribute +class Param { - public RequestParamType $type; + public Type $type; public string $paramName; + public array $validators; public string $description; public bool $required; - public array $validators; public bool $nullable; /** - * @param \App\Helpers\MetaFormats\RequestParamType $type The request parameter type (Post or Query). + * @param \App\Helpers\MetaFormats\Type $type The request parameter type (Post or Query). * @param string $name The name of the request parameter. + * @param array $validators An array of validators applied to the request parameter. * @param string $description The description of the request parameter. * @param bool $required Whether the request parameter is required. - * @param array $validators An array of validators applied to the request parameter. * @param bool $nullable Whether the request parameter can be null. */ public function __construct( - RequestParamType $type, + Type $type, string $name, + array $validators, string $description = "", bool $required = true, - array $validators = [], bool $nullable = false, ) { $this->type = $type; $this->paramName = $name; + $this->validators = $validators; $this->description = $description; $this->required = $required; - $this->validators = $validators; $this->nullable = $nullable; } } diff --git a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php index 87d8226d5..c79223b7f 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php @@ -5,33 +5,33 @@ use App\Helpers\MetaFormats\Attributes\FormatAttribute; use App\Helpers\MetaFormats\MetaFormat; use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute; -use App\Helpers\MetaFormats\RequestParamType; -use App\Helpers\MetaFormats\Validators\StringValidator; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VString; #[FormatAttribute(UserFormat::class)] class UserFormat extends MetaFormat { #[FormatAttribute("email")] - #[FormatParameterAttribute(type: RequestParamType::Post, description: "An email that will serve as a login name")] + #[FormatParameterAttribute(type: Type::Post, description: "An email that will serve as a login name")] public string $email; - #[FormatParameterAttribute(type: RequestParamType::Post, description: "First name")] + #[FormatParameterAttribute(type: Type::Post, description: "First name")] public string $firstName; - #[FormatParameterAttribute(type: RequestParamType::Post, description: "Last name", validators: [ new StringValidator(2) ])] + #[FormatParameterAttribute(type: Type::Post, description: "Last name", validators: [ new VString(2) ])] public string $lastName; - #[FormatParameterAttribute(type: RequestParamType::Post, description: "A password for authentication")] + #[FormatParameterAttribute(type: Type::Post, description: "A password for authentication")] public string $password; - #[FormatParameterAttribute(type: RequestParamType::Post, description: "A password confirmation")] + #[FormatParameterAttribute(type: Type::Post, description: "A password confirmation")] public string $passwordConfirm; - #[FormatParameterAttribute(type: RequestParamType::Post, description: "Identifier of the instance to register in")] + #[FormatParameterAttribute(type: Type::Post, description: "Identifier of the instance to register in")] public string $instanceId; #[FormatParameterAttribute( - type: RequestParamType::Post, + type: Type::Post, description: "Titles that are placed before user name", required: false, nullable: true @@ -39,7 +39,7 @@ class UserFormat extends MetaFormat public ?string $titlesBeforeName; #[FormatParameterAttribute( - type: RequestParamType::Post, + type: Type::Post, description: "Titles that are placed after user name", required: false, nullable: true diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 4684af767..f1eccf924 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -6,7 +6,7 @@ use App\Helpers\MetaFormats\Attributes\FormatAttribute; use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute; use App\Helpers\MetaFormats\Attributes\ParamAttribute; -use App\Helpers\MetaFormats\Attributes\RequestParamAttribute; +use App\Helpers\MetaFormats\Attributes\Param; use ReflectionClass; use App\Helpers\Swagger\AnnotationHelper; use ReflectionMethod; @@ -88,7 +88,7 @@ public static function extractFormatFromAttribute( */ public static function extractRequestParamData(ReflectionMethod $reflectionMethod): array { - $attrs = $reflectionMethod->getAttributes(RequestParamAttribute::class); + $attrs = $reflectionMethod->getAttributes(Param::class); $data = []; foreach ($attrs as $attr) { $paramAttr = $attr->newInstance(); diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 52af534b9..6fc511e73 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -3,13 +3,13 @@ namespace App\Helpers\MetaFormats; use App\Exceptions\InvalidArgumentException; -use App\Helpers\MetaFormats\Validators\ArrayValidator; -use App\Helpers\MetaFormats\Validators\StringValidator; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\Swagger\AnnotationParameterData; class RequestParamData { - public RequestParamType $type; + public Type $type; public string $name; public string $description; public bool $required; @@ -17,7 +17,7 @@ class RequestParamData public bool $nullable; public function __construct( - RequestParamType $type, + Type $type, string $name, string $description, bool $required, @@ -81,7 +81,7 @@ public function toAnnotationParameterData() $nestedArraySwaggerType = null; if ($this->hasValidators()) { $swaggerType = $this->validators[0]::SWAGGER_TYPE; - if ($this->validators[0] instanceof ArrayValidator) { + if ($this->validators[0] instanceof VArray) { $nestedArraySwaggerType = $this->validators[0]->getElementSwaggerType(); } } diff --git a/app/helpers/MetaFormats/RequestParamType.php b/app/helpers/MetaFormats/Type.php similarity index 89% rename from app/helpers/MetaFormats/RequestParamType.php rename to app/helpers/MetaFormats/Type.php index 735d487bb..5994b6506 100644 --- a/app/helpers/MetaFormats/RequestParamType.php +++ b/app/helpers/MetaFormats/Type.php @@ -6,7 +6,7 @@ /** * An enumeration of request parameter types. */ -enum RequestParamType +enum Type { case Post; case Query; diff --git a/app/helpers/MetaFormats/Validators/ArrayValidator.php b/app/helpers/MetaFormats/Validators/VArray.php similarity index 97% rename from app/helpers/MetaFormats/Validators/ArrayValidator.php rename to app/helpers/MetaFormats/Validators/VArray.php index 53062c6d1..111446289 100644 --- a/app/helpers/MetaFormats/Validators/ArrayValidator.php +++ b/app/helpers/MetaFormats/Validators/VArray.php @@ -5,7 +5,7 @@ use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; -class ArrayValidator +class VArray { public const SWAGGER_TYPE = "array"; diff --git a/app/helpers/MetaFormats/Validators/BoolValidator.php b/app/helpers/MetaFormats/Validators/VBool.php similarity index 93% rename from app/helpers/MetaFormats/Validators/BoolValidator.php rename to app/helpers/MetaFormats/Validators/VBool.php index 38c10cece..68c58c27f 100644 --- a/app/helpers/MetaFormats/Validators/BoolValidator.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -5,7 +5,7 @@ use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; -class BoolValidator +class VBool { public const SWAGGER_TYPE = "boolean"; diff --git a/app/helpers/MetaFormats/Validators/EmailValidator.php b/app/helpers/MetaFormats/Validators/VEmail.php similarity index 92% rename from app/helpers/MetaFormats/Validators/EmailValidator.php rename to app/helpers/MetaFormats/Validators/VEmail.php index 0ad876e16..73139d5fb 100644 --- a/app/helpers/MetaFormats/Validators/EmailValidator.php +++ b/app/helpers/MetaFormats/Validators/VEmail.php @@ -5,7 +5,7 @@ use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; -class EmailValidator extends StringValidator +class VEmail extends VString { public function __construct() { diff --git a/app/helpers/MetaFormats/Validators/FloatValidator.php b/app/helpers/MetaFormats/Validators/VFloat.php similarity index 93% rename from app/helpers/MetaFormats/Validators/FloatValidator.php rename to app/helpers/MetaFormats/Validators/VFloat.php index e79901874..148635ffd 100644 --- a/app/helpers/MetaFormats/Validators/FloatValidator.php +++ b/app/helpers/MetaFormats/Validators/VFloat.php @@ -5,7 +5,7 @@ use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; -class FloatValidator +class VFloat { public const SWAGGER_TYPE = "number"; diff --git a/app/helpers/MetaFormats/Validators/IntValidator.php b/app/helpers/MetaFormats/Validators/VInt.php similarity index 94% rename from app/helpers/MetaFormats/Validators/IntValidator.php rename to app/helpers/MetaFormats/Validators/VInt.php index caebf761a..953212e86 100644 --- a/app/helpers/MetaFormats/Validators/IntValidator.php +++ b/app/helpers/MetaFormats/Validators/VInt.php @@ -5,7 +5,7 @@ use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; -class IntValidator +class VInt { public const SWAGGER_TYPE = "integer"; diff --git a/app/helpers/MetaFormats/Validators/StringValidator.php b/app/helpers/MetaFormats/Validators/VString.php similarity index 97% rename from app/helpers/MetaFormats/Validators/StringValidator.php rename to app/helpers/MetaFormats/Validators/VString.php index 27b2e5fd7..635503fbe 100644 --- a/app/helpers/MetaFormats/Validators/StringValidator.php +++ b/app/helpers/MetaFormats/Validators/VString.php @@ -5,7 +5,7 @@ use App\Helpers\MetaFormats\MetaFormatHelper; use App\Helpers\MetaFormats\PhpTypes; -class StringValidator +class VString { public const SWAGGER_TYPE = "string"; private int $minLength; diff --git a/app/helpers/MetaFormats/Validators/TimestampValidator.php b/app/helpers/MetaFormats/Validators/VTimestamp.php similarity index 92% rename from app/helpers/MetaFormats/Validators/TimestampValidator.php rename to app/helpers/MetaFormats/Validators/VTimestamp.php index a56817808..4c1424689 100644 --- a/app/helpers/MetaFormats/Validators/TimestampValidator.php +++ b/app/helpers/MetaFormats/Validators/VTimestamp.php @@ -5,7 +5,7 @@ use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; -class TimestampValidator +class VTimestamp { public const SWAGGER_TYPE = "string"; diff --git a/app/helpers/MetaFormats/Validators/UuidValidator.php b/app/helpers/MetaFormats/Validators/VUuid.php similarity index 92% rename from app/helpers/MetaFormats/Validators/UuidValidator.php rename to app/helpers/MetaFormats/Validators/VUuid.php index aff087dfe..b2380d642 100644 --- a/app/helpers/MetaFormats/Validators/UuidValidator.php +++ b/app/helpers/MetaFormats/Validators/VUuid.php @@ -5,7 +5,7 @@ use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; -class UuidValidator extends StringValidator +class VUuid extends VString { public function __construct() { From b673fb8bb1d34e100e24600fafcbe13165e1910c Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 14 Feb 2025 12:20:26 +0100 Subject: [PATCH 026/103] added automatic multiline attribute conversions for long annotations --- .../AnnotationToAttributeConverter.php | 18 ++++++++++++++---- app/helpers/Swagger/ParenthesesBuilder.php | 19 ++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index 266174ced..96222f2d0 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -32,6 +32,8 @@ private static function shortenClass(string $className) return end($tokens); } + private static array $attributeParenthesesBuilders = []; + /** * Converts an array of preg_replace_callback matches to an attribute string. * @param array $matches An array of matches, with empty captures as NULL (PREG_UNMATCHED_AS_NULL flag). @@ -117,7 +119,8 @@ private static function regexCaptureToAttributeCallback(array $matches) } $paramAttributeClass = self::shortenClass(Param::class); - return "#[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; + self::$attributeParenthesesBuilders[] = $parenthesesBuilder; + return "#[{$paramAttributeClass}]"; } private static function checkValidationNullability(string $validation): bool @@ -230,6 +233,7 @@ public static function convertFile(string $path) { // read file and replace @Param annotations with attributes $content = file_get_contents($path); + self::$attributeParenthesesBuilders = []; $withInterleavedAttributes = preg_replace_callback(self::$postRegex, function ($matches) { return self::regexCaptureToAttributeCallback($matches); }, $content, -1, $count, PREG_UNMATCHED_AS_NULL); @@ -257,10 +261,16 @@ public static function convertFile(string $path) // detected the end of the comment block "*/", flush attribute lines } elseif (trim($line) === "*/") { $lines[] = $line; - foreach ($attributeLinesBuffer as $attributeLine) { - // the attribute lines are shifted by one space to the right (due to the comment block origin) - $lines[] = substr($attributeLine, 1); + for ($i = 0; $i < count($attributeLinesBuffer); $i++) { + $parenthesesBuilder = self::$attributeParenthesesBuilders[$i]; + $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; + // change to multiline if the line is too long + if (strlen($attributeLine) > 120) { + $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toMultilineString(4)}]"; + } + $lines[] = $attributeLine; } + $attributeLinesBuffer = []; } else { $lines[] = $line; diff --git a/app/helpers/Swagger/ParenthesesBuilder.php b/app/helpers/Swagger/ParenthesesBuilder.php index ed5c23746..eab3448c8 100644 --- a/app/helpers/Swagger/ParenthesesBuilder.php +++ b/app/helpers/Swagger/ParenthesesBuilder.php @@ -48,6 +48,23 @@ public function addValue(string $value): ParenthesesBuilder public function toString(): string { - return '(' . implode(', ', $this->tokens) . ')'; + return "(" . implode(", ", $this->tokens) . ")"; + } + + private static function spaces(int $count): string + { + return str_repeat(" ", $count); + } + + private const CODEBASE_INDENTATION = 4; + public function toMultilineString(int $initialIndentation): string + { + // do not add indentation to the first line + $str = "(\n"; + foreach ($this->tokens as $token) { + $str .= self::spaces($initialIndentation + self::CODEBASE_INDENTATION) . $token . ",\n"; + } + $str .= self::spaces($initialIndentation) . ")"; + return $str; } } From 3ae231d3458c94e59dece7457248f23014f3ba1b Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 14 Feb 2025 17:15:59 +0100 Subject: [PATCH 027/103] refactored attribute converter --- .../AnnotationToAttributeConverter.php | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index 96222f2d0..c986402f2 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -24,7 +24,15 @@ class AnnotationToAttributeConverter * The regex ends with '([a-z]+?=.+)\)', which is similar to the above, but instead of ending with * an optional comma etc., it ends with the closing parentheses of the @Param annotation. */ - private static string $postRegex = "/\*\s*@Param\((?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?([a-z]+?=.+)\)/"; + private static string $netteRegex = "/\*\s*@Param\((?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?([a-z]+?=.+)\)/"; + + /** + * A regex that matches standard PHP @param annotations of the <@param type $name description> format. + * There are three capture groups: type, name and description. + * The name does not contain the '$' prefix, and the description can contain '*', newline symbols, + * and extra spaces if multiline. + */ + private static string $standardRegex = "/\*\s*@param\s+(?\S*)\s+\$(?\S*)\s*(?.*(?:(?!\s*\*\s*(?:@|\/))(?:\s*\*\s*.*))*)/"; private static function shortenClass(string $className) { @@ -32,20 +40,13 @@ private static function shortenClass(string $className) return end($tokens); } - private static array $attributeParenthesesBuilders = []; - - /** - * Converts an array of preg_replace_callback matches to an attribute string. - * @param array $matches An array of matches, with empty captures as NULL (PREG_UNMATCHED_AS_NULL flag). - * @return string Returns an attribute string. - */ - private static function regexCaptureToAttributeCallback(array $matches) + private static function convertNetteRegexCapturesToParenthesesBuilder(array $captures) { // convert the string assignments in $matches to an associative array $annotationParameters = []; // the first element is the matched string - for ($i = 1; $i < count($matches); $i++) { - $capture = $matches[$i]; + for ($i = 1; $i < count($captures); $i++) { + $capture = $captures[$i]; if ($capture === null) { continue; } @@ -118,8 +119,20 @@ private static function regexCaptureToAttributeCallback(array $matches) $parenthesesBuilder->addValue("nullable: true"); } + return $parenthesesBuilder; + } + + /** + * Used by preg_replace_callback to replace captures with "#[Param]" strings to mark the lines for future + * replacement. Additionally stores the captures into an output array. + * @param array $captures An array of captures, with empty captures as NULL (PREG_UNMATCHED_AS_NULL flag). + * @param array $capturesList An output list for captures. + * @return string Returns "#[Param]". + */ + private static function netteRegexCaptureToAttributeCallback(array $captures, array &$capturesList) + { + $capturesList[] = $captures; $paramAttributeClass = self::shortenClass(Param::class); - self::$attributeParenthesesBuilders[] = $parenthesesBuilder; return "#[{$paramAttributeClass}]"; } @@ -233,10 +246,19 @@ public static function convertFile(string $path) { // read file and replace @Param annotations with attributes $content = file_get_contents($path); - self::$attributeParenthesesBuilders = []; - $withInterleavedAttributes = preg_replace_callback(self::$postRegex, function ($matches) { - return self::regexCaptureToAttributeCallback($matches); - }, $content, -1, $count, PREG_UNMATCHED_AS_NULL); + // Array that contains parentheses builders of all future generated attributes. + // Filled dynamically with the preg_replace_callback callback. + $capturesList = []; + $withInterleavedAttributes = preg_replace_callback( + self::$netteRegex, + function ($matches) use (&$capturesList) { + return self::netteRegexCaptureToAttributeCallback($matches, $capturesList); + }, + $content, + -1, + $count, + PREG_UNMATCHED_AS_NULL + ); // move the attribute lines below the comment block $lines = []; @@ -262,7 +284,7 @@ public static function convertFile(string $path) } elseif (trim($line) === "*/") { $lines[] = $line; for ($i = 0; $i < count($attributeLinesBuffer); $i++) { - $parenthesesBuilder = self::$attributeParenthesesBuilders[$i]; + $parenthesesBuilder = self::convertNetteRegexCapturesToParenthesesBuilder($capturesList[$i]); $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; // change to multiline if the line is too long if (strlen($attributeLine) > 120) { From 1498e7640cef8f55f8097a2624bb47fa15609760 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sat, 15 Feb 2025 12:33:17 +0100 Subject: [PATCH 028/103] added mechanism for path/query parameter detection WIP --- app/commands/SwaggerAnnotator.php | 114 ++------------ .../AnnotationToAttributeConverter.php | 148 +++++++++++++++--- app/helpers/Swagger/AnnotationHelper.php | 120 +++++++++++++- 3 files changed, 251 insertions(+), 131 deletions(-) diff --git a/app/commands/SwaggerAnnotator.php b/app/commands/SwaggerAnnotator.php index 1c9c9321b..5799f7253 100644 --- a/app/commands/SwaggerAnnotator.php +++ b/app/commands/SwaggerAnnotator.php @@ -21,7 +21,6 @@ class SwaggerAnnotator extends Command { protected static $defaultName = 'swagger:annotate'; - private static $presenterNamespace = 'App\V1Module\Presenters\\'; private static $autogeneratedAnnotationFilePath = 'app/V1Module/presenters/_autogenerated_annotations_temp.php'; protected function configure(): void @@ -41,18 +40,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int $fileBuilder->startClass('__Autogenerated_Annotation_Controller__', '1.0', 'ReCodEx API'); // get all routes of the api - $routes = $this->getRoutes(); - foreach ($routes as $routeObj) { - // extract class and method names of the endpoint - $metadata = $this->extractMetadata($routeObj); - $route = $this->extractRoute($routeObj); - $className = self::$presenterNamespace . $metadata['class']; - + $routesMetadata = AnnotationHelper::getRoutesMetadata(); + foreach ($routesMetadata as $route) { // extract data from the existing annotations - $annotationData = AnnotationHelper::extractAnnotationData($className, $metadata['method'], $route); + $annotationData = AnnotationHelper::extractAnnotationData( + $route["class"], + $route['method'], + $route["route"] + ); // add an empty method to the file with the transpiled annotations - $fileBuilder->addAnnotatedMethod($metadata['method'], $annotationData->toSwaggerAnnotations($route)); + $fileBuilder->addAnnotatedMethod( + $route['method'], + $annotationData->toSwaggerAnnotations($route["route"]) + ); } $fileBuilder->endClass(); @@ -63,97 +64,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } } - - /** - * Finds all route objects of the API - * @return array Returns an array of all found route objects. - */ - private function getRoutes(): array - { - $router = \App\V1Module\RouterFactory::createRouter(); - - // find all route object using a queue - $queue = [$router]; - $routes = []; - while (count($queue) != 0) { - $cursor = array_shift($queue); - - if ($cursor instanceof RouteList) { - foreach ($cursor->getRouters() as $item) { - // lists contain routes or nested lists - if ($item instanceof RouteList) { - array_push($queue, $item); - } else { - // the first route is special and holds no useful information for annotation - if (get_parent_class($item) !== MethodRoute::class) { - continue; - } - - $routes[] = $this->getPropertyValue($item, "route"); - } - } - } - } - - return $routes; - } - - /** - * Extracts the route string from a route object. Replaces '<..>' in the route with '{...}'. - * @param mixed $routeObj - */ - private function extractRoute($routeObj): string - { - $mask = self::getPropertyValue($routeObj, "mask"); - - // sample: replaces '/users/' with '/users/{id}' - $mask = str_replace(["<", ">"], ["{", "}"], $mask); - return "/" . $mask; - } - - /** - * Extracts the class and method names of the endpoint handler. - * @param mixed $routeObj The route object representing the endpoint. - * @return string[] Returns a dictionary [ "class" => ..., "method" => ...] - */ - private function extractMetadata($routeObj) - { - $metadata = self::getPropertyValue($routeObj, "metadata"); - $presenter = $metadata["presenter"]["value"]; - $action = $metadata["action"]["value"]; - - // if the name is empty, the method will be called 'actionDefault' - if ($action === null) { - $action = "default"; - } - - return [ - "class" => $presenter . "Presenter", - "method" => "action" . ucfirst($action), - ]; - } - - /** - * Helper function that can extract a property value from an arbitrary object where - * the property can be private. - * @param mixed $object The object to extract from. - * @param string $propertyName The name of the property. - * @return mixed Returns the value of the property. - */ - private static function getPropertyValue($object, string $propertyName): mixed - { - $class = new ReflectionClass($object); - - do { - try { - $property = $class->getProperty($propertyName); - } catch (ReflectionException $exception) { - $class = $class->getParentClass(); - $property = null; - } - } while ($property === null && $class !== null); - - $property->setAccessible(true); - return $property->getValue($object); - } } diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index c986402f2..2eaeba1f3 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -12,7 +12,9 @@ use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; +use App\Helpers\Swagger\AnnotationHelper; use App\Helpers\Swagger\ParenthesesBuilder; +use App\V1Module\Presenters\BasePresenter; class AnnotationToAttributeConverter { @@ -28,11 +30,21 @@ class AnnotationToAttributeConverter /** * A regex that matches standard PHP @param annotations of the <@param type $name description> format. - * There are three capture groups: type, name and description. + * There are three capture groups: validation (type), name and description. * The name does not contain the '$' prefix, and the description can contain '*', newline symbols, * and extra spaces if multiline. */ - private static string $standardRegex = "/\*\s*@param\s+(?\S*)\s+\$(?\S*)\s*(?.*(?:(?!\s*\*\s*(?:@|\/))(?:\s*\*\s*.*))*)/"; + private static string $standardRegex = "/\*\h*@param\h+(?\S*)\h+\\$(?\S*)\h*(?.*(?:(?!\s*\*\s*(?:@|\/))(?:\s*\*\s*.*))*)/"; + + // placeholder for detected nette annotations ("@Param") + // this text must not be present in the presenter files + private static string $netteAttributePlaceholder = "#nette#"; + // placeholder for detected standard php parameter annotations ("@param") + private static string $standardAttributePlaceholder = "#standard#"; + + // Metadata about endpoints used to determine what class methods are endpoints and what params + // are path and query. Initialized lazily (it cannot be assigned here because it is not a constant expression). + private static ?array $routesMetadata = null; private static function shortenClass(string $className) { @@ -40,9 +52,9 @@ private static function shortenClass(string $className) return end($tokens); } - private static function convertNetteRegexCapturesToParenthesesBuilder(array $captures) + private static function convertNetteRegexCapturesToDictionary(array $captures) { - // convert the string assignments in $matches to an associative array + // convert the string assignments in $captures to an associative array $annotationParameters = []; // the first element is the matched string for ($i = 1; $i < count($captures); $i++) { @@ -63,6 +75,33 @@ private static function convertNetteRegexCapturesToParenthesesBuilder(array $cap $annotationParameters[$key] = $value; } + return $annotationParameters; + } + + private static function convertStandardRegexCapturesToDictionary(array $captures) + { + ///TODO: add functionality to check whether the parameter is from query or path + $annotationParameters = []; + + if (!array_key_exists("validation", $captures)) { + throw new InternalServerException("Missing validation parameter."); + } + $annotationParameters["validation"] = $captures["validation"]; + + if (!array_key_exists("name", $captures)) { + throw new InternalServerException("Missing name parameter."); + } + $annotationParameters["name"] = $captures["name"]; + + if (array_key_exists("description", $captures)) { + $annotationParameters["description"] = $captures["description"]; + } + + return $annotationParameters; + } + + private static function convertRegexCapturesToParenthesesBuilder(array $annotationParameters) + { // serialize the parameters to an attribute $parenthesesBuilder = new ParenthesesBuilder(); @@ -123,17 +162,29 @@ private static function convertNetteRegexCapturesToParenthesesBuilder(array $cap } /** - * Used by preg_replace_callback to replace captures with "#[Param]" strings to mark the lines for future - * replacement. Additionally stores the captures into an output array. + * Used by preg_replace_callback to replace "@param" annotation captures with placeholder strings to + * mark the lines for future replacement. Additionally stores the captures into an output array. * @param array $captures An array of captures, with empty captures as NULL (PREG_UNMATCHED_AS_NULL flag). * @param array $capturesList An output list for captures. - * @return string Returns "#[Param]". + * @return string Returns a placeholder. + */ + private static function standardRegexCaptureToAttributeCallback(array $captures, array &$capturesList) + { + $capturesList[] = $captures; + return self::$standardAttributePlaceholder; + } + + /** + * Used by preg_replace_callback to replace "@Param" annotation captures with placeholder strings to mark the + * lines for future replacement. Additionally stores the captures into an output array. + * @param array $captures An array of captures, with empty captures as NULL (PREG_UNMATCHED_AS_NULL flag). + * @param array $capturesList An output list for captures. + * @return string Returns a placeholder. */ private static function netteRegexCaptureToAttributeCallback(array $captures, array &$capturesList) { $capturesList[] = $captures; - $paramAttributeClass = self::shortenClass(Param::class); - return "#[{$paramAttributeClass}]"; + return self::$netteAttributePlaceholder; } private static function checkValidationNullability(string $validation): bool @@ -242,27 +293,64 @@ private static function getValidatorNames() return $classNames; } + private static function preprocessFile(string $path) + { + if (self::$routesMetadata == null) { + self::$routesMetadata = AnnotationHelper::getRoutesMetadata(); + } + + // extract presenter namespace from BasePresenter + $namespaceTokens = explode("\\", BasePresenter::class); + $namespace = implode("\\", array_slice($namespaceTokens, 0, count($namespaceTokens) - 1)); + // join with presenter name from the file + $className = $namespace . "\\" . basename($path, ".php"); + + $endpoints = array_filter(self::$routesMetadata, function ($route) use ($className) { + return $route["class"] == $className; + }); + + foreach ($endpoints as $endpoint) { + $annotationData = AnnotationHelper::extractAnnotationData( + $endpoint["class"], + $endpoint["method"], + $endpoint["route"] + ); + var_dump($annotationData); + } + + } + public static function convertFile(string $path) { + self::preprocessFile($path); + return; // read file and replace @Param annotations with attributes $content = file_get_contents($path); // Array that contains parentheses builders of all future generated attributes. // Filled dynamically with the preg_replace_callback callback. - $capturesList = []; + $standardCapturesList = []; + $netteCapturesList = []; $withInterleavedAttributes = preg_replace_callback( - self::$netteRegex, - function ($matches) use (&$capturesList) { - return self::netteRegexCaptureToAttributeCallback($matches, $capturesList); + self::$standardRegex, + function ($matches) use (&$standardCapturesList) { + return self::standardRegexCaptureToAttributeCallback($matches, $standardCapturesList); }, $content, - -1, - $count, - PREG_UNMATCHED_AS_NULL + flags: PREG_UNMATCHED_AS_NULL ); + // $withInterleavedAttributes = preg_replace_callback( + // self::$netteRegex, + // function ($matches) use (&$netteCapturesList) { + // return self::netteRegexCaptureToAttributeCallback($matches, $netteCapturesList); + // }, + // $withInterleavedAttributes, + // flags: PREG_UNMATCHED_AS_NULL + // ); // move the attribute lines below the comment block $lines = []; - $attributeLinesBuffer = []; + $standardAttributeLinesCount = 0; + $netteAttributeLinesCount = 0; $usingsAdded = false; $paramAttributeClass = self::shortenClass(Param::class); $paramTypeClass = self::shortenClass(Type::class); @@ -277,14 +365,21 @@ function ($matches) use (&$capturesList) { // write the detected line (the first detected "use" line) $lines[] = $line; $usingsAdded = true; - // detected the new attribute line, store it in the buffer and do not write it yet - } elseif (preg_match("/#\[{$paramAttributeClass}/", $line) === 1) { - $attributeLinesBuffer[] = $line; + // detected an attribute line placeholder, increment the counter and remove the line + } elseif (str_contains($line, self::$standardAttributePlaceholder)) { + $standardAttributeLinesCount++; + } elseif (str_contains($line, self::$netteAttributePlaceholder)) { + $netteAttributeLinesCount++; // detected the end of the comment block "*/", flush attribute lines } elseif (trim($line) === "*/") { $lines[] = $line; - for ($i = 0; $i < count($attributeLinesBuffer); $i++) { - $parenthesesBuilder = self::convertNetteRegexCapturesToParenthesesBuilder($capturesList[$i]); + for ($i = 0; $i < $standardAttributeLinesCount; $i++) { + self::convertStandardRegexCapturesToDictionary($standardCapturesList[$i]); + ///TODO: implement rest of logic + } + for ($i = 0; $i < $netteAttributeLinesCount; $i++) { + $annotationParameters = self::convertNetteRegexCapturesToDictionary($netteCapturesList[$i]); + $parenthesesBuilder = self::convertRegexCapturesToParenthesesBuilder($annotationParameters); $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; // change to multiline if the line is too long if (strlen($attributeLine) > 120) { @@ -292,15 +387,16 @@ function ($matches) use (&$capturesList) { } $lines[] = $attributeLine; } - - $attributeLinesBuffer = []; + + // reset the counters for the next detected endpoint + ///TODO: these should not be reset (later captures will never be used) + $standardAttributeLinesCount = 0; + $netteAttributeLinesCount = 0; } else { $lines[] = $line; } } - ///TODO: add usings for used validators - ///TODO: handle too long lines return implode("\n", $lines); } } diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index 98495842e..a766a8e2c 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -3,15 +3,20 @@ namespace App\Helpers\Swagger; use App\Helpers\MetaFormats\MetaFormatHelper; +use App\V1Module\Router\MethodRoute; +use App\V1Module\RouterFactory; use ReflectionClass; +use ReflectionException; use ReflectionMethod; use Exception; +use Nette\Routing\RouteList; /** * Parser that can parse the annotations of existing recodex endpoints. */ class AnnotationHelper { + ///TODO: the null might be a prefix as well private static $nullableSuffix = '|null'; private static $typeMap = [ 'bool' => 'boolean', @@ -37,6 +42,8 @@ class AnnotationHelper 'upper' => 'string', ]; + private static $presenterNamespace = 'App\V1Module\Presenters\\'; + /** * Returns a ReflectionMethod object matching the name of the method and containing class. * @param string $className The name of the containing class. @@ -83,8 +90,8 @@ private static function isDatatypeNullable(string $annotationType): bool return false; } - // assumes that the typename ends with '|null' - if (str_ends_with($annotationType, self::$nullableSuffix)) { + // assumes that the typename contains 'null' + if (str_contains($annotationType, "null")) { return true; } @@ -350,4 +357,113 @@ public static function filterAnnotations(array $annotations, string $type) } return $rows; } + + /** + * Finds all route objects of the API and returns their metadata. + * @return array Returns an array of dictionaries with the keys "route", "class", and "method". + */ + public static function getRoutesMetadata(): array + { + $router = RouterFactory::createRouter(); + + // find all route object using a queue + $queue = [$router]; + $routes = []; + while (count($queue) != 0) { + $cursor = array_shift($queue); + + if ($cursor instanceof RouteList) { + foreach ($cursor->getRouters() as $item) { + // lists contain routes or nested lists + if ($item instanceof RouteList) { + array_push($queue, $item); + } else { + // the first route is special and holds no useful information for annotation + if (get_parent_class($item) !== MethodRoute::class) { + continue; + } + + $routes[] = self::getPropertyValue($item, "route"); + } + } + } + } + + + $routeMetadata = []; + foreach ($routes as $routeObj) { + // extract class and method names of the endpoint + $metadata = self::extractMetadata($routeObj); + $route = self::extractRoute($routeObj); + $className = self::$presenterNamespace . $metadata['class']; + $methodName = $metadata['method']; + + $routeMetadata[] = [ + "route" => $route, + "class" => $className, + "method" => $methodName, + ]; + } + + return $routeMetadata; + } + + /** + * Helper function that can extract a property value from an arbitrary object where + * the property can be private. + * @param mixed $object The object to extract from. + * @param string $propertyName The name of the property. + * @return mixed Returns the value of the property. + */ + public static function getPropertyValue(mixed $object, string $propertyName): mixed + { + $class = new ReflectionClass($object); + + do { + try { + $property = $class->getProperty($propertyName); + } catch (ReflectionException $exception) { + $class = $class->getParentClass(); + $property = null; + } + } while ($property === null && $class !== null); + + $property->setAccessible(true); + return $property->getValue($object); + } + + /** + * Extracts the route string from a route object. Replaces '<..>' in the route with '{...}'. + * @param mixed $routeObj + */ + private static function extractRoute($routeObj): string + { + $mask = AnnotationHelper::getPropertyValue($routeObj, "mask"); + + // sample: replaces '/users/' with '/users/{id}' + $mask = str_replace(["<", ">"], ["{", "}"], $mask); + return "/" . $mask; + } + + /** + * Extracts the class and method names of the endpoint handler. + * @param mixed $routeObj The route object representing the endpoint. + * @return string[] Returns a dictionary [ "class" => ..., "method" => ...] + */ + private static function extractMetadata($routeObj) + { + $metadata = AnnotationHelper::getPropertyValue($routeObj, "metadata"); + $presenter = $metadata["presenter"]["value"]; + $action = $metadata["action"]["value"]; + + // if the name is empty, the method will be called 'actionDefault' + if ($action === null) { + $action = "default"; + } + + return [ + "class" => $presenter . "Presenter", + "method" => "action" . ucfirst($action), + ]; + } } From c190b5fae04b02bf772bd642bbbf23d239cca3f3 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 16 Feb 2025 18:04:03 +0100 Subject: [PATCH 029/103] added support for @param annotation to attribute conversion --- .../AnnotationToAttributeConverter.php | 166 +++++++++++++++--- app/helpers/MetaFormats/Type.php | 1 + app/helpers/Swagger/AnnotationData.php | 5 + 3 files changed, 151 insertions(+), 21 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index 2eaeba1f3..c73c4dcfa 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -15,6 +15,7 @@ use App\Helpers\Swagger\AnnotationHelper; use App\Helpers\Swagger\ParenthesesBuilder; use App\V1Module\Presenters\BasePresenter; +use ReflectionMethod; class AnnotationToAttributeConverter { @@ -100,6 +101,13 @@ private static function convertStandardRegexCapturesToDictionary(array $captures return $annotationParameters; } + /** + * Convers an associative array into an attribute string builder. + * @param array $annotationParameters An associative array with a subset of the following keys: + * type, name, validation, description, required, nullable. + * @throws \App\Exceptions\InternalServerException + * @return ParenthesesBuilder A string builder used to build the final attribute string. + */ private static function convertRegexCapturesToParenthesesBuilder(array $annotationParameters) { // serialize the parameters to an attribute @@ -120,6 +128,9 @@ private static function convertRegexCapturesToParenthesesBuilder(array $annotati case "query": $type = $paramTypeClass . "::Query"; break; + case "path": + $type = $paramTypeClass . "::Path"; + break; default: throw new InternalServerException("Unknown request type: $typeStr"); } @@ -250,6 +261,7 @@ private static function convertAnnotationValidationToValidatorString(string $val $validatorClass = VEmail::class; break; case "numericint": + case "integer": $validatorClass = VInt::class; break; case "bool": @@ -305,47 +317,159 @@ private static function preprocessFile(string $path) // join with presenter name from the file $className = $namespace . "\\" . basename($path, ".php"); + // get endpoint metadata for this file $endpoints = array_filter(self::$routesMetadata, function ($route) use ($className) { return $route["class"] == $className; }); + // add info about where the method starts + foreach ($endpoints as &$endpoint) { + $reflectionMethod = new ReflectionMethod($endpoint["class"], $endpoint["method"]); + // the method returns the line indexed from 1 + $endpoint["startLine"] = $reflectionMethod->getStartLine() - 1; + $endpoint["endLine"] = $reflectionMethod->getEndLine() - 1; + } + + // sort endpoint based on position in the file (so that the file preprocessing can be done top-down) + $startLines = array_column($endpoints, "startLine"); + array_multisort($startLines, SORT_ASC, $endpoints); + + // get file lines + $content = file_get_contents($path); + $lines = self::fileStringToLines($content); + + // maps certain line indices to replacement annotation blocks and their extends + $annotationReplacements = []; + foreach ($endpoints as $endpoint) { + $class = $endpoint["class"]; + $method = $endpoint["method"]; + $route = $endpoint["route"]; + $startLine = $endpoint["startLine"]; + + // get info about endpoint parameters and their types $annotationData = AnnotationHelper::extractAnnotationData( - $endpoint["class"], - $endpoint["method"], - $endpoint["route"] + $class, + $method, + $route ); - var_dump($annotationData); + + // find start and end lines of method annotations + $annotationEndLine = $startLine - 1; + $annotationStartLine = -1; + for ($i = $annotationEndLine - 1; $i >= 0; $i--) { + if (str_contains($lines[$i], "/**")) { + $annotationStartLine = $i; + break; + } + } + if ($annotationStartLine == -1) { + throw new InternalServerException("Could not find annotation start line"); + } + + $annotationLines = array_slice($lines, $annotationStartLine, $annotationEndLine - $annotationStartLine + 1); + $params = $annotationData->getAllParams(); + + /// attempt to remove param lines, but it is too complicated (handle missing param lines + multiline params) + // foreach ($params as $param) { + // // matches the line containing the parameter name with word boundaries + // $paramLineRegex = "/\\$\\b" . $param->name . "\\b/"; + // $lineIdx = -1; + // for ($i = 0; $i < count($annotationLines); $i++) { + // if (preg_match($paramLineRegex, $annotationLines[$i]) == 1) { + // $lineIdx = $i; + // break; + // } + // } + // } + + // crate an attribute from each parameter + foreach ($params as $param) { + $data = [ + "name" => $param->name, + "validation" => $param->swaggerType, + "type" => $param->location, + "required" => ($param->required ? "true" : "false"), + "nullable" => ($param->nullable ? "true" : "false"), + ]; + if ($param->description != null) { + $data["description"] = $param->description; + } + + $builder = self::convertRegexCapturesToParenthesesBuilder($data); + $paramAttributeClass = self::shortenClass(Param::class); + $attributeLine = " #[{$paramAttributeClass}{$builder->toString()}]"; + // change to multiline if the line is too long + if (strlen($attributeLine) > 120) { + $attributeLine = " #[{$paramAttributeClass}{$builder->toMultilineString(4)}]"; + } + + // append the attribute line to the existing annotations + $annotationLines[] = $attributeLine; + } + + $annotationReplacements[$annotationStartLine] = [ + "annotations" => $annotationLines, + "originalAnnotationEndLine" => $annotationEndLine, + ]; } + $newLines = []; + for ($i = 0; $i < count($lines); $i++) { + // copy non-annotation lines + if (!array_key_exists($i, $annotationReplacements)) { + $newLines[] = $lines[$i]; + continue; + } + + // add new annotations + foreach ($annotationReplacements[$i]["annotations"] as $replacementLine) { + $newLines[] = $replacementLine; + } + // move $i to the original annotation end line + $i = $annotationReplacements[$i]["originalAnnotationEndLine"]; + } + + return self::linesToFileString($newLines); + } + + private static function fileStringToLines(string $fileContent): array + { + $lines = preg_split("/((\r?\n)|(\r\n?))/", $fileContent); + if ($lines == false) { + throw new InternalServerException("File content cannot be split into lines"); + } + return $lines; + } + + private static function linesToFileString(array $lines): string + { + return implode("\n", $lines); } public static function convertFile(string $path) { - self::preprocessFile($path); - return; - // read file and replace @Param annotations with attributes - $content = file_get_contents($path); + $content = self::preprocessFile($path); // Array that contains parentheses builders of all future generated attributes. // Filled dynamically with the preg_replace_callback callback. $standardCapturesList = []; $netteCapturesList = []; + // $withInterleavedAttributes = preg_replace_callback( + // self::$standardRegex, + // function ($matches) use (&$standardCapturesList) { + // return self::standardRegexCaptureToAttributeCallback($matches, $standardCapturesList); + // }, + // $content, + // flags: PREG_UNMATCHED_AS_NULL + // ); $withInterleavedAttributes = preg_replace_callback( - self::$standardRegex, - function ($matches) use (&$standardCapturesList) { - return self::standardRegexCaptureToAttributeCallback($matches, $standardCapturesList); + self::$netteRegex, + function ($matches) use (&$netteCapturesList) { + return self::netteRegexCaptureToAttributeCallback($matches, $netteCapturesList); }, $content, flags: PREG_UNMATCHED_AS_NULL ); - // $withInterleavedAttributes = preg_replace_callback( - // self::$netteRegex, - // function ($matches) use (&$netteCapturesList) { - // return self::netteRegexCaptureToAttributeCallback($matches, $netteCapturesList); - // }, - // $withInterleavedAttributes, - // flags: PREG_UNMATCHED_AS_NULL - // ); // move the attribute lines below the comment block $lines = []; @@ -354,7 +478,7 @@ function ($matches) use (&$standardCapturesList) { $usingsAdded = false; $paramAttributeClass = self::shortenClass(Param::class); $paramTypeClass = self::shortenClass(Type::class); - foreach (preg_split("/((\r?\n)|(\r\n?))/", $withInterleavedAttributes) as $line) { + foreach (self::fileStringToLines($withInterleavedAttributes) as $line) { // detected the initial "use" block, add usings for new types if (!$usingsAdded && strlen($line) > 3 && substr($line, 0, 3) === "use") { $lines[] = "use App\\Helpers\\MetaFormats\\Attributes\\{$paramAttributeClass};"; @@ -397,6 +521,6 @@ function ($matches) use (&$standardCapturesList) { } } - return implode("\n", $lines); + return self::linesToFileString($lines); } } diff --git a/app/helpers/MetaFormats/Type.php b/app/helpers/MetaFormats/Type.php index 5994b6506..32b5f06b9 100644 --- a/app/helpers/MetaFormats/Type.php +++ b/app/helpers/MetaFormats/Type.php @@ -10,5 +10,6 @@ enum Type { case Post; case Query; + case Path; } // @codingStandardsIgnoreEnd diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index 154b79f52..31c781bd1 100644 --- a/app/helpers/Swagger/AnnotationData.php +++ b/app/helpers/Swagger/AnnotationData.php @@ -28,6 +28,11 @@ public function __construct( $this->endpointDescription = $endpointDescription; } + public function getAllParams(): array + { + return array_merge($this->pathParams, $this->queryParams, $this->bodyParams); + } + /** * Creates a method annotation string parsable by the swagger generator. * Example: if the method name is 'Put', the method will return '@OA\\PUT'. From 5bed864a5a5b1c5e1c8382ee8040e46e1af7964d Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 16 Feb 2025 18:08:52 +0100 Subject: [PATCH 030/103] removed deprecated conversion mechanism --- .../AnnotationToAttributeConverter.php | 61 +------------------ 1 file changed, 1 insertion(+), 60 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index c73c4dcfa..7053fc5f6 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -29,14 +29,6 @@ class AnnotationToAttributeConverter */ private static string $netteRegex = "/\*\s*@Param\((?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?([a-z]+?=.+)\)/"; - /** - * A regex that matches standard PHP @param annotations of the <@param type $name description> format. - * There are three capture groups: validation (type), name and description. - * The name does not contain the '$' prefix, and the description can contain '*', newline symbols, - * and extra spaces if multiline. - */ - private static string $standardRegex = "/\*\h*@param\h+(?\S*)\h+\\$(?\S*)\h*(?.*(?:(?!\s*\*\s*(?:@|\/))(?:\s*\*\s*.*))*)/"; - // placeholder for detected nette annotations ("@Param") // this text must not be present in the presenter files private static string $netteAttributePlaceholder = "#nette#"; @@ -79,28 +71,6 @@ private static function convertNetteRegexCapturesToDictionary(array $captures) return $annotationParameters; } - private static function convertStandardRegexCapturesToDictionary(array $captures) - { - ///TODO: add functionality to check whether the parameter is from query or path - $annotationParameters = []; - - if (!array_key_exists("validation", $captures)) { - throw new InternalServerException("Missing validation parameter."); - } - $annotationParameters["validation"] = $captures["validation"]; - - if (!array_key_exists("name", $captures)) { - throw new InternalServerException("Missing name parameter."); - } - $annotationParameters["name"] = $captures["name"]; - - if (array_key_exists("description", $captures)) { - $annotationParameters["description"] = $captures["description"]; - } - - return $annotationParameters; - } - /** * Convers an associative array into an attribute string builder. * @param array $annotationParameters An associative array with a subset of the following keys: @@ -172,19 +142,6 @@ private static function convertRegexCapturesToParenthesesBuilder(array $annotati return $parenthesesBuilder; } - /** - * Used by preg_replace_callback to replace "@param" annotation captures with placeholder strings to - * mark the lines for future replacement. Additionally stores the captures into an output array. - * @param array $captures An array of captures, with empty captures as NULL (PREG_UNMATCHED_AS_NULL flag). - * @param array $capturesList An output list for captures. - * @return string Returns a placeholder. - */ - private static function standardRegexCaptureToAttributeCallback(array $captures, array &$capturesList) - { - $capturesList[] = $captures; - return self::$standardAttributePlaceholder; - } - /** * Used by preg_replace_callback to replace "@Param" annotation captures with placeholder strings to mark the * lines for future replacement. Additionally stores the captures into an output array. @@ -452,16 +409,8 @@ public static function convertFile(string $path) $content = self::preprocessFile($path); // Array that contains parentheses builders of all future generated attributes. // Filled dynamically with the preg_replace_callback callback. - $standardCapturesList = []; $netteCapturesList = []; - // $withInterleavedAttributes = preg_replace_callback( - // self::$standardRegex, - // function ($matches) use (&$standardCapturesList) { - // return self::standardRegexCaptureToAttributeCallback($matches, $standardCapturesList); - // }, - // $content, - // flags: PREG_UNMATCHED_AS_NULL - // ); + $withInterleavedAttributes = preg_replace_callback( self::$netteRegex, function ($matches) use (&$netteCapturesList) { @@ -473,7 +422,6 @@ function ($matches) use (&$netteCapturesList) { // move the attribute lines below the comment block $lines = []; - $standardAttributeLinesCount = 0; $netteAttributeLinesCount = 0; $usingsAdded = false; $paramAttributeClass = self::shortenClass(Param::class); @@ -490,17 +438,11 @@ function ($matches) use (&$netteCapturesList) { $lines[] = $line; $usingsAdded = true; // detected an attribute line placeholder, increment the counter and remove the line - } elseif (str_contains($line, self::$standardAttributePlaceholder)) { - $standardAttributeLinesCount++; } elseif (str_contains($line, self::$netteAttributePlaceholder)) { $netteAttributeLinesCount++; // detected the end of the comment block "*/", flush attribute lines } elseif (trim($line) === "*/") { $lines[] = $line; - for ($i = 0; $i < $standardAttributeLinesCount; $i++) { - self::convertStandardRegexCapturesToDictionary($standardCapturesList[$i]); - ///TODO: implement rest of logic - } for ($i = 0; $i < $netteAttributeLinesCount; $i++) { $annotationParameters = self::convertNetteRegexCapturesToDictionary($netteCapturesList[$i]); $parenthesesBuilder = self::convertRegexCapturesToParenthesesBuilder($annotationParameters); @@ -514,7 +456,6 @@ function ($matches) use (&$netteCapturesList) { // reset the counters for the next detected endpoint ///TODO: these should not be reset (later captures will never be used) - $standardAttributeLinesCount = 0; $netteAttributeLinesCount = 0; } else { $lines[] = $line; From d6f0d0999b297a6f283d6615e5f41c797273d75c Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 16 Feb 2025 18:22:17 +0100 Subject: [PATCH 031/103] bugfix: converted attributes are no longer used multiple times --- .../MetaFormats/AnnotationToAttributeConverter.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php index 7053fc5f6..d8418e188 100644 --- a/app/helpers/MetaFormats/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationToAttributeConverter.php @@ -32,11 +32,9 @@ class AnnotationToAttributeConverter // placeholder for detected nette annotations ("@Param") // this text must not be present in the presenter files private static string $netteAttributePlaceholder = "#nette#"; - // placeholder for detected standard php parameter annotations ("@param") - private static string $standardAttributePlaceholder = "#standard#"; // Metadata about endpoints used to determine what class methods are endpoints and what params - // are path and query. Initialized lazily (it cannot be assigned here because it is not a constant expression). + // are path and query. Initialized lazily (it cannot be assigned here because it is not a constant expression). private static ?array $routesMetadata = null; private static function shortenClass(string $className) @@ -453,9 +451,10 @@ function ($matches) use (&$netteCapturesList) { } $lines[] = $attributeLine; } - + + // remove the captures used in this endpoint + $netteCapturesList = array_slice($netteCapturesList, $netteAttributeLinesCount); // reset the counters for the next detected endpoint - ///TODO: these should not be reset (later captures will never be used) $netteAttributeLinesCount = 0; } else { $lines[] = $line; From 3b4c253c2b3da693a91b59f57919cba850d68017 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 16 Feb 2025 18:56:08 +0100 Subject: [PATCH 032/103] restructured code --- app/commands/MetaTester.php | 2 +- .../AnnotationToAttributeConverter.php | 63 +++ .../NetteAnnotationConverter.php | 265 ++++++++++ .../StandardAnnotationConverter.php | 144 ++++++ .../AnnotationConversion/Utils.php | 53 ++ .../AnnotationToAttributeConverter.php | 466 ------------------ 6 files changed, 526 insertions(+), 467 deletions(-) create mode 100644 app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php create mode 100644 app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php create mode 100644 app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php create mode 100644 app/helpers/MetaFormats/AnnotationConversion/Utils.php delete mode 100644 app/helpers/MetaFormats/AnnotationToAttributeConverter.php diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php index f04dc116c..c0c13fdf5 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaTester.php @@ -2,7 +2,7 @@ namespace App\Console; -use App\Helpers\MetaFormats\AnnotationToAttributeConverter; +use App\Helpers\MetaFormats\AnnotationConversion\AnnotationToAttributeConverter; use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute; use App\Helpers\MetaFormats\FormatDefinitions\GroupFormat; use App\Helpers\MetaFormats\FormatDefinitions\UserFormat; diff --git a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php new file mode 100644 index 000000000..e215dd190 --- /dev/null +++ b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php @@ -0,0 +1,63 @@ + 3 && substr($line, 0, 3) === "use") { + $lines[] = "use App\\Helpers\\MetaFormats\\Attributes\\{$paramAttributeClass};"; + $lines[] = "use App\\Helpers\\MetaFormats\\{$paramTypeClass};"; + foreach (Utils::getValidatorNames() as $validator) { + $lines[] = "use App\\Helpers\\MetaFormats\\Validators\\{$validator};"; + } + // write the detected line (the first detected "use" line) + $lines[] = $line; + $usingsAdded = true; + // detected an attribute line placeholder, increment the counter and remove the line + } elseif (str_contains($line, NetteAnnotationConverter::$netteAttributePlaceholder)) { + $netteAttributeLinesCount++; + // detected the end of the comment block "*/", flush attribute lines + } elseif (trim($line) === "*/") { + $lines[] = $line; + for ($i = 0; $i < $netteAttributeLinesCount; $i++) { + $annotationParameters = NetteAnnotationConverter::convertNetteRegexCapturesToDictionary($netteCapturesList[$i]); + $parenthesesBuilder = NetteAnnotationConverter::convertRegexCapturesToParenthesesBuilder($annotationParameters); + $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; + // change to multiline if the line is too long + if (strlen($attributeLine) > 120) { + $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toMultilineString(4)}]"; + } + $lines[] = $attributeLine; + } + + // remove the captures used in this endpoint + $netteCapturesList = array_slice($netteCapturesList, $netteAttributeLinesCount); + // reset the counters for the next detected endpoint + $netteAttributeLinesCount = 0; + } else { + $lines[] = $line; + } + } + + return Utils::linesToFileString($lines); + } +} diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php new file mode 100644 index 000000000..a175d1bd5 --- /dev/null +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -0,0 +1,265 @@ +#nette#"; + + public static function regexReplaceAnnotations(string $fileContent) + { + // Array that contains parentheses builders of all future generated attributes. + // Filled dynamically with the preg_replace_callback callback. + $netteCapturesList = []; + + $withPlaceholders = preg_replace_callback( + self::$netteRegex, + function ($matches) use (&$netteCapturesList) { + return self::netteRegexCaptureToAttributeCallback($matches, $netteCapturesList); + }, + $fileContent, + flags: PREG_UNMATCHED_AS_NULL + ); + + return [ + "contentWithPlaceholders" => $withPlaceholders, + "captures" => $netteCapturesList, + ]; + } + + public static function convertNetteRegexCapturesToDictionary(array $captures) + { + // convert the string assignments in $captures to an associative array + $annotationParameters = []; + // the first element is the matched string + for ($i = 1; $i < count($captures); $i++) { + $capture = $captures[$i]; + if ($capture === null) { + continue; + } + + // the regex extracts the key as the first capture, and the value as the second or third (depends + // whether the value is enclosed in double quotes) + $parseResult = preg_match('/([a-z]+)=(?:(?:"(.+?)")|(?:(.+)))/', $capture, $tokens, PREG_UNMATCHED_AS_NULL); + if ($parseResult !== 1) { + throw new InternalServerException("Unexpected assignment format: $capture"); + } + + $key = $tokens[1]; + $value = $tokens[2] ?? $tokens[3]; + $annotationParameters[$key] = $value; + } + + return $annotationParameters; + } + + /** + * Used by preg_replace_callback to replace "@Param" annotation captures with placeholder strings to mark the + * lines for future replacement. Additionally stores the captures into an output array. + * @param array $captures An array of captures, with empty captures as NULL (PREG_UNMATCHED_AS_NULL flag). + * @param array $capturesList An output list for captures. + * @return string Returns a placeholder. + */ + private static function netteRegexCaptureToAttributeCallback(array $captures, array &$capturesList) + { + $capturesList[] = $captures; + return self::$netteAttributePlaceholder; + } + + /** + * @return string[] Returns an array of Validator class names (without the namespace). + */ + private static function getValidatorNames() + { + $dir = __DIR__ . "/Validators"; + $baseFilenames = scandir($dir); + $classNames = []; + foreach ($baseFilenames as $filename) { + if (!str_ends_with($filename, ".php")) { + continue; + } + + // remove the ".php" suffix + $className = substr($filename, 0, -4); + $classNames[] = $className; + } + return $classNames; + } + + /** + * Converts annotation validation values (such as "string:1..255") to Validator construction + * strings (such as "new VString(1, 255)"). + * @param string $validation The annotation validation string. + * @return string Returns the object construction string. + */ + private static function convertAnnotationValidationToValidatorString(string $validation): string + { + if (str_starts_with($validation, "string")) { + $stringValidator = Utils::shortenClass(VString::class); + + // handle string length constraints, such as "string:1..255" + if (strlen($validation) > 6) { + if ($validation[6] !== ":") { + throw new InternalServerException("Unknown string validation format: $validation"); + } + $suffix = substr($validation, 7); + + // special case for uuids + if ($suffix === "36") { + return "new " . Utils::shortenClass(VUuid::class) . "()"; + } + + // capture the two bounding numbers and the double dot in strings of + // types "1..255", "..255", "1..", or "255" + if (preg_match("/([0-9]*)(..)?([0-9]+)?/", $suffix, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw new InternalServerException("Unknown string validation format: $validation"); + } + + // type "255", exact match + if ($matches[2] === null) { + return "new {$stringValidator}({$matches[1]}, {$matches[1]})"; + // type "1..255" + } elseif ($matches[1] !== null && $matches[3] !== null) { + return "new {$stringValidator}({$matches[1]}, {$matches[3]})"; + // type "..255" + } elseif ($matches[1] === null) { + return "new {$stringValidator}(0, {$matches[3]})"; + // type "1.." + } elseif ($matches[3] === null) { + return "new {$stringValidator}({$matches[1]})"; + } + + throw new InternalServerException("Unknown string validation format: $validation"); + } + + return "new {$stringValidator}()"; + } + + // non-string validation rules do not have parameters, so they can be converted directly + $validatorClass = null; + switch ($validation) { + case "email": + // there is one occurrence of this + case "email:1..": + $validatorClass = VEmail::class; + break; + case "numericint": + case "integer": + $validatorClass = VInt::class; + break; + case "bool": + case "boolean": + $validatorClass = VBool::class; + break; + case "array": + case "list": + $validatorClass = VArray::class; + break; + case "timestamp": + $validatorClass = VTimestamp::class; + break; + case "numeric": + $validatorClass = VFloat::class; + break; + default: + throw new InternalServerException("Unknown validation rule: $validation"); + } + + return "new " . Utils::shortenClass($validatorClass) . "()"; + } + + /** + * Convers an associative array into an attribute string builder. + * @param array $annotationParameters An associative array with a subset of the following keys: + * type, name, validation, description, required, nullable. + * @throws \App\Exceptions\InternalServerException + * @return ParenthesesBuilder A string builder used to build the final attribute string. + */ + public static function convertRegexCapturesToParenthesesBuilder(array $annotationParameters) + { + // serialize the parameters to an attribute + $parenthesesBuilder = new ParenthesesBuilder(); + + // add type + if (!array_key_exists("type", $annotationParameters)) { + throw new InternalServerException("Missing type parameter."); + } + + $typeStr = $annotationParameters["type"]; + $paramTypeClass = Utils::shortenClass(Type::class); + $type = null; + switch ($typeStr) { + case "post": + $type = $paramTypeClass . "::Post"; + break; + case "query": + $type = $paramTypeClass . "::Query"; + break; + case "path": + $type = $paramTypeClass . "::Path"; + break; + default: + throw new InternalServerException("Unknown request type: $typeStr"); + } + $parenthesesBuilder->addValue($type); + + // add name + if (!array_key_exists("name", $annotationParameters)) { + throw new InternalServerException("Missing name parameter."); + } + $parenthesesBuilder->addValue("\"{$annotationParameters["name"]}\""); + + $nullable = false; + if (array_key_exists("validation", $annotationParameters)) { + $validation = $annotationParameters["validation"]; + + if (Utils::checkValidationNullability($validation)) { + // remove the '|null' from the end of the string + $validation = substr($validation, 0, -5); + $nullable = true; + } + + // this will always produce a single validator (the annotations do not contain multiple validation fields) + $validator = self::convertAnnotationValidationToValidatorString($validation); + $parenthesesBuilder->addValue(value: "[ $validator ]"); + } + + if (array_key_exists("description", $annotationParameters)) { + $parenthesesBuilder->addValue("\"{$annotationParameters["description"]}\""); + } + + if (array_key_exists("required", $annotationParameters)) { + $parenthesesBuilder->addValue("required: " . $annotationParameters["required"]); + } + + if ($nullable) { + $parenthesesBuilder->addValue("nullable: true"); + } + + return $parenthesesBuilder; + } +} diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php new file mode 100644 index 000000000..8e80acd5f --- /dev/null +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -0,0 +1,144 @@ +getStartLine() - 1; + $endpoint["endLine"] = $reflectionMethod->getEndLine() - 1; + } + + // sort endpoint based on position in the file (so that the file preprocessing can be done top-down) + $startLines = array_column($endpoints, "startLine"); + array_multisort($startLines, SORT_ASC, $endpoints); + + // get file lines + $content = file_get_contents($path); + $lines = Utils::fileStringToLines($content); + + // maps certain line indices to replacement annotation blocks and their extends + $annotationReplacements = []; + + foreach ($endpoints as $endpoint) { + $class = $endpoint["class"]; + $method = $endpoint["method"]; + $route = $endpoint["route"]; + $startLine = $endpoint["startLine"]; + + // get info about endpoint parameters and their types + $annotationData = AnnotationHelper::extractAnnotationData( + $class, + $method, + $route + ); + + // find start and end lines of method annotations + $annotationEndLine = $startLine - 1; + $annotationStartLine = -1; + for ($i = $annotationEndLine - 1; $i >= 0; $i--) { + if (str_contains($lines[$i], "/**")) { + $annotationStartLine = $i; + break; + } + } + if ($annotationStartLine == -1) { + throw new InternalServerException("Could not find annotation start line"); + } + + $annotationLines = array_slice($lines, $annotationStartLine, $annotationEndLine - $annotationStartLine + 1); + $params = $annotationData->getAllParams(); + + /// attempt to remove param lines, but it is too complicated (handle missing param lines + multiline params) + // foreach ($params as $param) { + // // matches the line containing the parameter name with word boundaries + // $paramLineRegex = "/\\$\\b" . $param->name . "\\b/"; + // $lineIdx = -1; + // for ($i = 0; $i < count($annotationLines); $i++) { + // if (preg_match($paramLineRegex, $annotationLines[$i]) == 1) { + // $lineIdx = $i; + // break; + // } + // } + // } + + // crate an attribute from each parameter + foreach ($params as $param) { + $data = [ + "name" => $param->name, + "validation" => $param->swaggerType, + "type" => $param->location, + "required" => ($param->required ? "true" : "false"), + "nullable" => ($param->nullable ? "true" : "false"), + ]; + if ($param->description != null) { + $data["description"] = $param->description; + } + + $builder = NetteAnnotationConverter::convertRegexCapturesToParenthesesBuilder($data); + $paramAttributeClass = Utils::shortenClass(Param::class); + $attributeLine = " #[{$paramAttributeClass}{$builder->toString()}]"; + // change to multiline if the line is too long + if (strlen($attributeLine) > 120) { + $attributeLine = " #[{$paramAttributeClass}{$builder->toMultilineString(4)}]"; + } + + // append the attribute line to the existing annotations + $annotationLines[] = $attributeLine; + } + + $annotationReplacements[$annotationStartLine] = [ + "annotations" => $annotationLines, + "originalAnnotationEndLine" => $annotationEndLine, + ]; + } + + $newLines = []; + for ($i = 0; $i < count($lines); $i++) { + // copy non-annotation lines + if (!array_key_exists($i, $annotationReplacements)) { + $newLines[] = $lines[$i]; + continue; + } + + // add new annotations + foreach ($annotationReplacements[$i]["annotations"] as $replacementLine) { + $newLines[] = $replacementLine; + } + // move $i to the original annotation end line + $i = $annotationReplacements[$i]["originalAnnotationEndLine"]; + } + + return Utils::linesToFileString($newLines); + } +} diff --git a/app/helpers/MetaFormats/AnnotationConversion/Utils.php b/app/helpers/MetaFormats/AnnotationConversion/Utils.php new file mode 100644 index 000000000..cb60bf272 --- /dev/null +++ b/app/helpers/MetaFormats/AnnotationConversion/Utils.php @@ -0,0 +1,53 @@ +#nette#"; - - // Metadata about endpoints used to determine what class methods are endpoints and what params - // are path and query. Initialized lazily (it cannot be assigned here because it is not a constant expression). - private static ?array $routesMetadata = null; - - private static function shortenClass(string $className) - { - $tokens = explode("\\", $className); - return end($tokens); - } - - private static function convertNetteRegexCapturesToDictionary(array $captures) - { - // convert the string assignments in $captures to an associative array - $annotationParameters = []; - // the first element is the matched string - for ($i = 1; $i < count($captures); $i++) { - $capture = $captures[$i]; - if ($capture === null) { - continue; - } - - // the regex extracts the key as the first capture, and the value as the second or third (depends - // whether the value is enclosed in double quotes) - $parseResult = preg_match('/([a-z]+)=(?:(?:"(.+?)")|(?:(.+)))/', $capture, $tokens, PREG_UNMATCHED_AS_NULL); - if ($parseResult !== 1) { - throw new InternalServerException("Unexpected assignment format: $capture"); - } - - $key = $tokens[1]; - $value = $tokens[2] ?? $tokens[3]; - $annotationParameters[$key] = $value; - } - - return $annotationParameters; - } - - /** - * Convers an associative array into an attribute string builder. - * @param array $annotationParameters An associative array with a subset of the following keys: - * type, name, validation, description, required, nullable. - * @throws \App\Exceptions\InternalServerException - * @return ParenthesesBuilder A string builder used to build the final attribute string. - */ - private static function convertRegexCapturesToParenthesesBuilder(array $annotationParameters) - { - // serialize the parameters to an attribute - $parenthesesBuilder = new ParenthesesBuilder(); - - // add type - if (!array_key_exists("type", $annotationParameters)) { - throw new InternalServerException("Missing type parameter."); - } - - $typeStr = $annotationParameters["type"]; - $paramTypeClass = self::shortenClass(Type::class); - $type = null; - switch ($typeStr) { - case "post": - $type = $paramTypeClass . "::Post"; - break; - case "query": - $type = $paramTypeClass . "::Query"; - break; - case "path": - $type = $paramTypeClass . "::Path"; - break; - default: - throw new InternalServerException("Unknown request type: $typeStr"); - } - $parenthesesBuilder->addValue($type); - - // add name - if (!array_key_exists("name", $annotationParameters)) { - throw new InternalServerException("Missing name parameter."); - } - $parenthesesBuilder->addValue("\"{$annotationParameters["name"]}\""); - - $nullable = false; - if (array_key_exists("validation", $annotationParameters)) { - $validation = $annotationParameters["validation"]; - - if (self::checkValidationNullability($validation)) { - // remove the '|null' from the end of the string - $validation = substr($validation, 0, -5); - $nullable = true; - } - - // this will always produce a single validator (the annotations do not contain multiple validation fields) - $validator = self::convertAnnotationValidationToValidatorString($validation); - $parenthesesBuilder->addValue(value: "[ $validator ]"); - } - - if (array_key_exists("description", $annotationParameters)) { - $parenthesesBuilder->addValue("\"{$annotationParameters["description"]}\""); - } - - if (array_key_exists("required", $annotationParameters)) { - $parenthesesBuilder->addValue("required: " . $annotationParameters["required"]); - } - - if ($nullable) { - $parenthesesBuilder->addValue("nullable: true"); - } - - return $parenthesesBuilder; - } - - /** - * Used by preg_replace_callback to replace "@Param" annotation captures with placeholder strings to mark the - * lines for future replacement. Additionally stores the captures into an output array. - * @param array $captures An array of captures, with empty captures as NULL (PREG_UNMATCHED_AS_NULL flag). - * @param array $capturesList An output list for captures. - * @return string Returns a placeholder. - */ - private static function netteRegexCaptureToAttributeCallback(array $captures, array &$capturesList) - { - $capturesList[] = $captures; - return self::$netteAttributePlaceholder; - } - - private static function checkValidationNullability(string $validation): bool - { - return str_ends_with($validation, "|null"); - } - - /** - * Converts annotation validation values (such as "string:1..255") to Validator construction - * strings (such as "new VString(1, 255)"). - * @param string $validation The annotation validation string. - * @return string Returns the object construction string. - */ - private static function convertAnnotationValidationToValidatorString(string $validation): string - { - if (str_starts_with($validation, "string")) { - $stringValidator = self::shortenClass(VString::class); - - // handle string length constraints, such as "string:1..255" - if (strlen($validation) > 6) { - if ($validation[6] !== ":") { - throw new InternalServerException("Unknown string validation format: $validation"); - } - $suffix = substr($validation, 7); - - // special case for uuids - if ($suffix === "36") { - return "new " . self::shortenClass(VUuid::class) . "()"; - } - - // capture the two bounding numbers and the double dot in strings of - // types "1..255", "..255", "1..", or "255" - if (preg_match("/([0-9]*)(..)?([0-9]+)?/", $suffix, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { - throw new InternalServerException("Unknown string validation format: $validation"); - } - - // type "255", exact match - if ($matches[2] === null) { - return "new {$stringValidator}({$matches[1]}, {$matches[1]})"; - // type "1..255" - } elseif ($matches[1] !== null && $matches[3] !== null) { - return "new {$stringValidator}({$matches[1]}, {$matches[3]})"; - // type "..255" - } elseif ($matches[1] === null) { - return "new {$stringValidator}(0, {$matches[3]})"; - // type "1.." - } elseif ($matches[3] === null) { - return "new {$stringValidator}({$matches[1]})"; - } - - throw new InternalServerException("Unknown string validation format: $validation"); - } - - return "new {$stringValidator}()"; - } - - // non-string validation rules do not have parameters, so they can be converted directly - $validatorClass = null; - switch ($validation) { - case "email": - // there is one occurrence of this - case "email:1..": - $validatorClass = VEmail::class; - break; - case "numericint": - case "integer": - $validatorClass = VInt::class; - break; - case "bool": - case "boolean": - $validatorClass = VBool::class; - break; - case "array": - case "list": - $validatorClass = VArray::class; - break; - case "timestamp": - $validatorClass = VTimestamp::class; - break; - case "numeric": - $validatorClass = VFloat::class; - break; - default: - throw new InternalServerException("Unknown validation rule: $validation"); - } - - return "new " . self::shortenClass($validatorClass) . "()"; - } - - /** - * @return string[] Returns an array of Validator class names (without the namespace). - */ - private static function getValidatorNames() - { - $dir = __DIR__ . "/Validators"; - $baseFilenames = scandir($dir); - $classNames = []; - foreach ($baseFilenames as $filename) { - if (!str_ends_with($filename, ".php")) { - continue; - } - - // remove the ".php" suffix - $className = substr($filename, 0, -4); - $classNames[] = $className; - } - return $classNames; - } - - private static function preprocessFile(string $path) - { - if (self::$routesMetadata == null) { - self::$routesMetadata = AnnotationHelper::getRoutesMetadata(); - } - - // extract presenter namespace from BasePresenter - $namespaceTokens = explode("\\", BasePresenter::class); - $namespace = implode("\\", array_slice($namespaceTokens, 0, count($namespaceTokens) - 1)); - // join with presenter name from the file - $className = $namespace . "\\" . basename($path, ".php"); - - // get endpoint metadata for this file - $endpoints = array_filter(self::$routesMetadata, function ($route) use ($className) { - return $route["class"] == $className; - }); - - // add info about where the method starts - foreach ($endpoints as &$endpoint) { - $reflectionMethod = new ReflectionMethod($endpoint["class"], $endpoint["method"]); - // the method returns the line indexed from 1 - $endpoint["startLine"] = $reflectionMethod->getStartLine() - 1; - $endpoint["endLine"] = $reflectionMethod->getEndLine() - 1; - } - - // sort endpoint based on position in the file (so that the file preprocessing can be done top-down) - $startLines = array_column($endpoints, "startLine"); - array_multisort($startLines, SORT_ASC, $endpoints); - - // get file lines - $content = file_get_contents($path); - $lines = self::fileStringToLines($content); - - // maps certain line indices to replacement annotation blocks and their extends - $annotationReplacements = []; - - foreach ($endpoints as $endpoint) { - $class = $endpoint["class"]; - $method = $endpoint["method"]; - $route = $endpoint["route"]; - $startLine = $endpoint["startLine"]; - - // get info about endpoint parameters and their types - $annotationData = AnnotationHelper::extractAnnotationData( - $class, - $method, - $route - ); - - // find start and end lines of method annotations - $annotationEndLine = $startLine - 1; - $annotationStartLine = -1; - for ($i = $annotationEndLine - 1; $i >= 0; $i--) { - if (str_contains($lines[$i], "/**")) { - $annotationStartLine = $i; - break; - } - } - if ($annotationStartLine == -1) { - throw new InternalServerException("Could not find annotation start line"); - } - - $annotationLines = array_slice($lines, $annotationStartLine, $annotationEndLine - $annotationStartLine + 1); - $params = $annotationData->getAllParams(); - - /// attempt to remove param lines, but it is too complicated (handle missing param lines + multiline params) - // foreach ($params as $param) { - // // matches the line containing the parameter name with word boundaries - // $paramLineRegex = "/\\$\\b" . $param->name . "\\b/"; - // $lineIdx = -1; - // for ($i = 0; $i < count($annotationLines); $i++) { - // if (preg_match($paramLineRegex, $annotationLines[$i]) == 1) { - // $lineIdx = $i; - // break; - // } - // } - // } - - // crate an attribute from each parameter - foreach ($params as $param) { - $data = [ - "name" => $param->name, - "validation" => $param->swaggerType, - "type" => $param->location, - "required" => ($param->required ? "true" : "false"), - "nullable" => ($param->nullable ? "true" : "false"), - ]; - if ($param->description != null) { - $data["description"] = $param->description; - } - - $builder = self::convertRegexCapturesToParenthesesBuilder($data); - $paramAttributeClass = self::shortenClass(Param::class); - $attributeLine = " #[{$paramAttributeClass}{$builder->toString()}]"; - // change to multiline if the line is too long - if (strlen($attributeLine) > 120) { - $attributeLine = " #[{$paramAttributeClass}{$builder->toMultilineString(4)}]"; - } - - // append the attribute line to the existing annotations - $annotationLines[] = $attributeLine; - } - - $annotationReplacements[$annotationStartLine] = [ - "annotations" => $annotationLines, - "originalAnnotationEndLine" => $annotationEndLine, - ]; - } - - $newLines = []; - for ($i = 0; $i < count($lines); $i++) { - // copy non-annotation lines - if (!array_key_exists($i, $annotationReplacements)) { - $newLines[] = $lines[$i]; - continue; - } - - // add new annotations - foreach ($annotationReplacements[$i]["annotations"] as $replacementLine) { - $newLines[] = $replacementLine; - } - // move $i to the original annotation end line - $i = $annotationReplacements[$i]["originalAnnotationEndLine"]; - } - - return self::linesToFileString($newLines); - } - - private static function fileStringToLines(string $fileContent): array - { - $lines = preg_split("/((\r?\n)|(\r\n?))/", $fileContent); - if ($lines == false) { - throw new InternalServerException("File content cannot be split into lines"); - } - return $lines; - } - - private static function linesToFileString(array $lines): string - { - return implode("\n", $lines); - } - - public static function convertFile(string $path) - { - $content = self::preprocessFile($path); - // Array that contains parentheses builders of all future generated attributes. - // Filled dynamically with the preg_replace_callback callback. - $netteCapturesList = []; - - $withInterleavedAttributes = preg_replace_callback( - self::$netteRegex, - function ($matches) use (&$netteCapturesList) { - return self::netteRegexCaptureToAttributeCallback($matches, $netteCapturesList); - }, - $content, - flags: PREG_UNMATCHED_AS_NULL - ); - - // move the attribute lines below the comment block - $lines = []; - $netteAttributeLinesCount = 0; - $usingsAdded = false; - $paramAttributeClass = self::shortenClass(Param::class); - $paramTypeClass = self::shortenClass(Type::class); - foreach (self::fileStringToLines($withInterleavedAttributes) as $line) { - // detected the initial "use" block, add usings for new types - if (!$usingsAdded && strlen($line) > 3 && substr($line, 0, 3) === "use") { - $lines[] = "use App\\Helpers\\MetaFormats\\Attributes\\{$paramAttributeClass};"; - $lines[] = "use App\\Helpers\\MetaFormats\\{$paramTypeClass};"; - foreach (self::getValidatorNames() as $validator) { - $lines[] = "use App\\Helpers\\MetaFormats\\Validators\\{$validator};"; - } - // write the detected line (the first detected "use" line) - $lines[] = $line; - $usingsAdded = true; - // detected an attribute line placeholder, increment the counter and remove the line - } elseif (str_contains($line, self::$netteAttributePlaceholder)) { - $netteAttributeLinesCount++; - // detected the end of the comment block "*/", flush attribute lines - } elseif (trim($line) === "*/") { - $lines[] = $line; - for ($i = 0; $i < $netteAttributeLinesCount; $i++) { - $annotationParameters = self::convertNetteRegexCapturesToDictionary($netteCapturesList[$i]); - $parenthesesBuilder = self::convertRegexCapturesToParenthesesBuilder($annotationParameters); - $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; - // change to multiline if the line is too long - if (strlen($attributeLine) > 120) { - $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toMultilineString(4)}]"; - } - $lines[] = $attributeLine; - } - - // remove the captures used in this endpoint - $netteCapturesList = array_slice($netteCapturesList, $netteAttributeLinesCount); - // reset the counters for the next detected endpoint - $netteAttributeLinesCount = 0; - } else { - $lines[] = $line; - } - } - - return self::linesToFileString($lines); - } -} From 585dda5429d141066fae7814637cf1cf06c3aa1f Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 16 Feb 2025 20:03:31 +0100 Subject: [PATCH 033/103] improved code quality, added more comments --- .../AnnotationToAttributeConverter.php | 20 ++- .../NetteAnnotationConverter.php | 85 ++++++----- .../StandardAnnotationConverter.php | 133 +++++++++++------- .../AnnotationConversion/Utils.php | 32 +++++ 4 files changed, 170 insertions(+), 100 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php index e215dd190..8bd600546 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php @@ -7,9 +7,14 @@ class AnnotationToAttributeConverter { - public static function convertFile(string $path) + /** + * Converts the endpoint annotations in a presenter class file into attributes. + * @param string $path The path to the presenter. + * @return string Returns the converted file content as a string. + */ + public static function convertFile(string $path): string { - $content = StandardAnnotationConverter::preprocessFile($path); + $content = StandardAnnotationConverter::convertStandardAnnotations($path); $nettePreprocess = NetteAnnotationConverter::regexReplaceAnnotations($content); $netteCapturesList = $nettePreprocess["captures"]; @@ -33,20 +38,13 @@ public static function convertFile(string $path) $lines[] = $line; $usingsAdded = true; // detected an attribute line placeholder, increment the counter and remove the line - } elseif (str_contains($line, NetteAnnotationConverter::$netteAttributePlaceholder)) { + } elseif (str_contains($line, NetteAnnotationConverter::$attributePlaceholder)) { $netteAttributeLinesCount++; // detected the end of the comment block "*/", flush attribute lines } elseif (trim($line) === "*/") { $lines[] = $line; for ($i = 0; $i < $netteAttributeLinesCount; $i++) { - $annotationParameters = NetteAnnotationConverter::convertNetteRegexCapturesToDictionary($netteCapturesList[$i]); - $parenthesesBuilder = NetteAnnotationConverter::convertRegexCapturesToParenthesesBuilder($annotationParameters); - $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; - // change to multiline if the line is too long - if (strlen($attributeLine) > 120) { - $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toMultilineString(4)}]"; - } - $lines[] = $attributeLine; + $lines[] = NetteAnnotationConverter::convertCapturesToAttributeString($netteCapturesList[$i]); } // remove the captures used in this endpoint diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index a175d1bd5..5d632036b 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -3,6 +3,7 @@ namespace App\Helpers\MetaFormats\AnnotationConversion; use App\Exceptions\InternalServerException; +use App\Helpers\MetaFormats\Attributes\Param; use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; @@ -20,40 +21,72 @@ class NetteAnnotationConverter * A regex that matches @Param annotations and captures its parameters. Can capture up to 7 parameters. * Contains 6 copies of the following sub-regex: '(?:([a-z]+?=.+?),?\s*\*?\s*)?', which * matches 'name=value' assignments followed by an optional comma, whitespace, - * star (multi-line annotation support), whitespace. The capture contains only 'name=value'. + * star (multi-line annotation support), and whitespace. The capture contains only 'name=value'. * The regex ends with '([a-z]+?=.+)\)', which is similar to the above, but instead of ending with * an optional comma etc., it ends with the closing parentheses of the @Param annotation. */ - private static string $netteRegex = "/\*\s*@Param\((?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?([a-z]+?=.+)\)/"; + private static string $paramRegex = "/\*\s*@Param\((?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?([a-z]+?=.+)\)/"; - // placeholder for detected nette annotations ("@Param") + // placeholder for detected nette annotations (prefixed with "@Param") // this text must not be present in the presenter files - public static string $netteAttributePlaceholder = "#nette#"; + public static string $attributePlaceholder = "#nette#"; + + /** + * Replaces "@Param" annotations with placeholders and extracts its data. + * @param string $fileContent The file content to be replaced. + * @return array{captures: array, contentWithPlaceholders: string} Returns the content with placeholders and the + * extracted data. + */ public static function regexReplaceAnnotations(string $fileContent) { // Array that contains parentheses builders of all future generated attributes. // Filled dynamically with the preg_replace_callback callback. - $netteCapturesList = []; + $captures = []; - $withPlaceholders = preg_replace_callback( - self::$netteRegex, - function ($matches) use (&$netteCapturesList) { - return self::netteRegexCaptureToAttributeCallback($matches, $netteCapturesList); + $contentWithPlaceholders = preg_replace_callback( + self::$paramRegex, + function ($matches) use (&$captures) { + return self::regexCaptureToAttributeCallback($matches, $captures); }, $fileContent, flags: PREG_UNMATCHED_AS_NULL ); return [ - "contentWithPlaceholders" => $withPlaceholders, - "captures" => $netteCapturesList, + "contentWithPlaceholders" => $contentWithPlaceholders, + "captures" => $captures, ]; } + + /** + * Converts regex parameter captures to an attribute string. + * @param array $captures Regex parameter captures. + * @return string Returns the attribute string. + */ + public static function convertCapturesToAttributeString(array $captures) + { + $paramAttributeClass = Utils::shortenClass(Param::class); + + $annotationParameters = NetteAnnotationConverter::convertCapturesToDictionary($captures); + $parenthesesBuilder = NetteAnnotationConverter::convertRegexCapturesToParenthesesBuilder($annotationParameters); + $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; + // change to multiline if the line is too long + if (strlen($attributeLine) > 120) { + $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toMultilineString(4)}]"; + } + return $attributeLine; + } - public static function convertNetteRegexCapturesToDictionary(array $captures) + /** + * Converts regex parameter captures into a dictionary. + * @param array $captures The regex captures. + * @throws \App\Exceptions\InternalServerException + * @return array Returns a dictionary with field names as keys pointing to values. + */ + private static function convertCapturesToDictionary(array $captures) { - // convert the string assignments in $captures to an associative array + // convert the string assignments in $captures to a dictionary $annotationParameters = []; // the first element is the matched string for ($i = 1; $i < count($captures); $i++) { @@ -84,30 +117,10 @@ public static function convertNetteRegexCapturesToDictionary(array $captures) * @param array $capturesList An output list for captures. * @return string Returns a placeholder. */ - private static function netteRegexCaptureToAttributeCallback(array $captures, array &$capturesList) + private static function regexCaptureToAttributeCallback(array $captures, array &$capturesList) { $capturesList[] = $captures; - return self::$netteAttributePlaceholder; - } - - /** - * @return string[] Returns an array of Validator class names (without the namespace). - */ - private static function getValidatorNames() - { - $dir = __DIR__ . "/Validators"; - $baseFilenames = scandir($dir); - $classNames = []; - foreach ($baseFilenames as $filename) { - if (!str_ends_with($filename, ".php")) { - continue; - } - - // remove the ".php" suffix - $className = substr($filename, 0, -4); - $classNames[] = $className; - } - return $classNames; + return self::$attributePlaceholder; } /** @@ -193,7 +206,7 @@ private static function convertAnnotationValidationToValidatorString(string $val } /** - * Convers an associative array into an attribute string builder. + * Convers a parameter dictionary into an attribute string builder. * @param array $annotationParameters An associative array with a subset of the following keys: * type, name, validation, description, required, nullable. * @throws \App\Exceptions\InternalServerException diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php index 8e80acd5f..6e9fe1368 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -5,6 +5,7 @@ use App\Exceptions\InternalServerException; use App\Helpers\MetaFormats\Attributes\Param; use App\Helpers\Swagger\AnnotationHelper; +use App\Helpers\Swagger\AnnotationParameterData; use App\V1Module\Presenters\BasePresenter; use ReflectionMethod; @@ -14,24 +15,29 @@ class StandardAnnotationConverter // are path and query. Initialized lazily (it cannot be assigned here because it is not a constant expression). private static ?array $routesMetadata = null; - public static function preprocessFile(string $path) + /** + * Converts standard PHP annotations (@param) of a presenter to attributes. + * @param string $path The path to the presenter file. + * @throws \App\Exceptions\InternalServerException + * @return string Returns the converted presenter file content. + */ + public static function convertStandardAnnotations(string $path): string { + // initialize the metadata structure if (self::$routesMetadata == null) { self::$routesMetadata = AnnotationHelper::getRoutesMetadata(); } - // extract presenter namespace from BasePresenter - $namespaceTokens = explode("\\", BasePresenter::class); - $namespace = implode("\\", array_slice($namespaceTokens, 0, count($namespaceTokens) - 1)); - // join with presenter name from the file - $className = $namespace . "\\" . basename($path, ".php"); + // get fully qualified class name of the presenter + $presenterNamespace = Utils::getPresenterNamespace(); + $className = $presenterNamespace . "\\" . basename($path, ".php"); // get endpoint metadata for this file $endpoints = array_filter(self::$routesMetadata, function ($route) use ($className) { return $route["class"] == $className; }); - // add info about where the method starts + // add info about where the method starts and ends foreach ($endpoints as &$endpoint) { $reflectionMethod = new ReflectionMethod($endpoint["class"], $endpoint["method"]); // the method returns the line indexed from 1 @@ -39,7 +45,7 @@ public static function preprocessFile(string $path) $endpoint["endLine"] = $reflectionMethod->getEndLine() - 1; } - // sort endpoint based on position in the file (so that the file preprocessing can be done top-down) + // sort endpoints based on position in the file (so that the file preprocessing can be done top-down) $startLines = array_column($endpoints, "startLine"); array_multisort($startLines, SORT_ASC, $endpoints); @@ -47,24 +53,50 @@ public static function preprocessFile(string $path) $content = file_get_contents($path); $lines = Utils::fileStringToLines($content); - // maps certain line indices to replacement annotation blocks and their extends - $annotationReplacements = []; + // creates a list of replacement annotation blocks and their extends, keyed by original annotation start lines + $annotationReplacements = self::convertEndpointAnnotations($endpoints, $lines); - foreach ($endpoints as $endpoint) { - $class = $endpoint["class"]; - $method = $endpoint["method"]; - $route = $endpoint["route"]; - $startLine = $endpoint["startLine"]; + // replace original annotations with the new ones + $newLines = []; + for ($i = 0; $i < count($lines); $i++) { + // copy non-annotation lines + if (!array_key_exists($i, $annotationReplacements)) { + $newLines[] = $lines[$i]; + continue; + } + + // add new annotations + foreach ($annotationReplacements[$i]["annotations"] as $replacementLine) { + $newLines[] = $replacementLine; + } + // move $i to the original annotation end line (skip original annotations) + $i = $annotationReplacements[$i]["originalAnnotationEndLine"]; + } + return Utils::linesToFileString($newLines); + } + + /** + * Converts endpoint annotations to annotations with parameter attributes. + * @param array $endpoints Endpoint method metadata sorted by line number. + * @param array $lines Lines of the file to be converted. + * @throws \App\Exceptions\InternalServerException + * @return array} a list of dictionaries + * containing the new annotation lines and the end line of the original annotations. + */ + private static function convertEndpointAnnotations(array $endpoints, array $lines): array + { + $annotationReplacements = []; + foreach ($endpoints as $endpoint) { // get info about endpoint parameters and their types $annotationData = AnnotationHelper::extractAnnotationData( - $class, - $method, - $route + $endpoint["class"], + $endpoint["method"], + $endpoint["route"] ); // find start and end lines of method annotations - $annotationEndLine = $startLine - 1; + $annotationEndLine = $endpoint["startLine"] - 1; $annotationStartLine = -1; for ($i = $annotationEndLine - 1; $i >= 0; $i--) { if (str_contains($lines[$i], "/**")) { @@ -94,27 +126,8 @@ public static function preprocessFile(string $path) // crate an attribute from each parameter foreach ($params as $param) { - $data = [ - "name" => $param->name, - "validation" => $param->swaggerType, - "type" => $param->location, - "required" => ($param->required ? "true" : "false"), - "nullable" => ($param->nullable ? "true" : "false"), - ]; - if ($param->description != null) { - $data["description"] = $param->description; - } - - $builder = NetteAnnotationConverter::convertRegexCapturesToParenthesesBuilder($data); - $paramAttributeClass = Utils::shortenClass(Param::class); - $attributeLine = " #[{$paramAttributeClass}{$builder->toString()}]"; - // change to multiline if the line is too long - if (strlen($attributeLine) > 120) { - $attributeLine = " #[{$paramAttributeClass}{$builder->toMultilineString(4)}]"; - } - // append the attribute line to the existing annotations - $annotationLines[] = $attributeLine; + $annotationLines[] = self::getAttributeLineFromMetadata($param); } $annotationReplacements[$annotationStartLine] = [ @@ -123,22 +136,36 @@ public static function preprocessFile(string $path) ]; } - $newLines = []; - for ($i = 0; $i < count($lines); $i++) { - // copy non-annotation lines - if (!array_key_exists($i, $annotationReplacements)) { - $newLines[] = $lines[$i]; - continue; - } + return $annotationReplacements; + } - // add new annotations - foreach ($annotationReplacements[$i]["annotations"] as $replacementLine) { - $newLines[] = $replacementLine; - } - // move $i to the original annotation end line - $i = $annotationReplacements[$i]["originalAnnotationEndLine"]; + /** + * Converts parameter metadata into an attribute string. + * @param \App\Helpers\Swagger\AnnotationParameterData $param The parameter metadata. + * @return string The attribute string. + */ + private static function getAttributeLineFromMetadata(AnnotationParameterData $param): string + { + // convert metadata to nette regex capture dictionary + $data = [ + "name" => $param->name, + "validation" => $param->swaggerType, + "type" => $param->location, + "required" => ($param->required ? "true" : "false"), + "nullable" => ($param->nullable ? "true" : "false"), + ]; + if ($param->description != null) { + $data["description"] = $param->description; } - return Utils::linesToFileString($newLines); + $builder = NetteAnnotationConverter::convertRegexCapturesToParenthesesBuilder($data); + $paramAttributeClass = Utils::shortenClass(Param::class); + $attributeLine = " #[{$paramAttributeClass}{$builder->toString()}]"; + // change to multiline if the line is too long + if (strlen($attributeLine) > 120) { + $attributeLine = " #[{$paramAttributeClass}{$builder->toMultilineString(4)}]"; + } + + return $attributeLine; } } diff --git a/app/helpers/MetaFormats/AnnotationConversion/Utils.php b/app/helpers/MetaFormats/AnnotationConversion/Utils.php index cb60bf272..06bead867 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/Utils.php +++ b/app/helpers/MetaFormats/AnnotationConversion/Utils.php @@ -3,20 +3,39 @@ namespace App\Helpers\MetaFormats\AnnotationConversion; use App\Exceptions\InternalServerException; +use App\V1Module\Presenters\BasePresenter; class Utils { + /** + * Converts a fully qualified class name to a class name without namespace prefixes. + * @param string $className Fully qualified class name, such + * as "App\Helpers\MetaFormats\AnnotationConversion\Utils". + * @return string Class name without namespace prefixes, such as "Utils". + */ public static function shortenClass(string $className) { $tokens = explode("\\", $className); return end($tokens); } + /** + * Checks whether the validation string ends with the "|null" suffix. + * Validation strings contain the "null" qualifier always at the end of the string. + * @param string $validation The validation string. + * @return bool Returns whether the validation ends with "|null". + */ public static function checkValidationNullability(string $validation): bool { return str_ends_with($validation, "|null"); } + /** + * Splits a string into lines. + * @param string $fileContent The string to be split. + * @throws \App\Exceptions\InternalServerException Thrown when the string cannot be split. + * @return array The lines of the string. + */ public static function fileStringToLines(string $fileContent): array { $lines = preg_split("/((\r?\n)|(\r\n?))/", $fileContent); @@ -26,6 +45,11 @@ public static function fileStringToLines(string $fileContent): array return $lines; } + /** + * Joins an array of strings into a single string separated by '\n'. + * @param array $lines The lines to be joined. + * @return string The joined string. + */ public static function linesToFileString(array $lines): string { return implode("\n", $lines); @@ -50,4 +74,12 @@ public static function getValidatorNames() } return $classNames; } + + public static function getPresenterNamespace() + { + // extract presenter namespace from BasePresenter + $namespaceTokens = explode("\\", BasePresenter::class); + $namespace = implode("\\", array_slice($namespaceTokens, 0, count($namespaceTokens) - 1)); + return $namespace; + } } From 2f06e9a7a30e34cd1b0a17a9fece7bac5fbf897c Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sun, 16 Feb 2025 20:14:48 +0100 Subject: [PATCH 034/103] missing validations are defaulted to string --- .../NetteAnnotationConverter.php | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index 5d632036b..c31cf0aa4 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -246,21 +246,23 @@ public static function convertRegexCapturesToParenthesesBuilder(array $annotatio } $parenthesesBuilder->addValue("\"{$annotationParameters["name"]}\""); + // replace missing validations with string validations + if (!array_key_exists("validation", $annotationParameters)) { + $annotationParameters["validation"] = "string"; + } $nullable = false; - if (array_key_exists("validation", $annotationParameters)) { - $validation = $annotationParameters["validation"]; - - if (Utils::checkValidationNullability($validation)) { - // remove the '|null' from the end of the string - $validation = substr($validation, 0, -5); - $nullable = true; - } + $validation = $annotationParameters["validation"]; - // this will always produce a single validator (the annotations do not contain multiple validation fields) - $validator = self::convertAnnotationValidationToValidatorString($validation); - $parenthesesBuilder->addValue(value: "[ $validator ]"); + if (Utils::checkValidationNullability($validation)) { + // remove the '|null' from the end of the string + $validation = substr($validation, 0, -5); + $nullable = true; } + // this will always produce a single validator (the annotations do not contain multiple validation fields) + $validator = self::convertAnnotationValidationToValidatorString($validation); + $parenthesesBuilder->addValue(value: "[ $validator ]"); + if (array_key_exists("description", $annotationParameters)) { $parenthesesBuilder->addValue("\"{$annotationParameters["description"]}\""); } From 8de1c28be293b65df57cf3a298786370e4a6f612 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Mon, 17 Feb 2025 12:38:53 +0100 Subject: [PATCH 035/103] single validators no longer need to be in arrays --- .../presenters/base/BasePresenter.php | 2 +- .../NetteAnnotationConverter.php | 3 +- .../Attributes/FormatParameterAttribute.php | 6 ++-- app/helpers/MetaFormats/Attributes/Param.php | 6 ++-- app/helpers/MetaFormats/RequestParamData.php | 30 +++++++++++++++---- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 8f169978b..adee900bf 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -206,7 +206,7 @@ protected function isInScope(string $scope): bool return $identity->isInScope($scope); } - public function getMetaRequest(): MetaRequest|null + public function getMetaRequest(): MetaRequest | null { if ($this->requestFormatInstance === null) { throw new InternalServerException( diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index c31cf0aa4..93dc5a821 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -258,10 +258,9 @@ public static function convertRegexCapturesToParenthesesBuilder(array $annotatio $validation = substr($validation, 0, -5); $nullable = true; } - // this will always produce a single validator (the annotations do not contain multiple validation fields) $validator = self::convertAnnotationValidationToValidatorString($validation); - $parenthesesBuilder->addValue(value: "[ $validator ]"); + $parenthesesBuilder->addValue(value: $validator); if (array_key_exists("description", $annotationParameters)) { $parenthesesBuilder->addValue("\"{$annotationParameters["description"]}\""); diff --git a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php index 271ebc27c..c354d1a73 100644 --- a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php +++ b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php @@ -12,7 +12,7 @@ class FormatParameterAttribute { public Type $type; - public array $validators; + public mixed $validators; public string $description; public bool $required; // there is not an easy way to check whether a property has the nullability flag set @@ -20,14 +20,14 @@ class FormatParameterAttribute /** * @param \App\Helpers\MetaFormats\Type $type The request parameter type (Post or Query). - * @param array $validators An array of validators applied to the request parameter. + * @param mixed $validators A validator object or an array of validators applied to the request parameter. * @param string $description The description of the request parameter. * @param bool $required Whether the request parameter is required. * @param bool $nullable Whether the request parameter can be null. */ public function __construct( Type $type, - array $validators = [], + mixed $validators = [], string $description = "", bool $required = true, bool $nullable = false, diff --git a/app/helpers/MetaFormats/Attributes/Param.php b/app/helpers/MetaFormats/Attributes/Param.php index 1e926bcd8..349c313e4 100644 --- a/app/helpers/MetaFormats/Attributes/Param.php +++ b/app/helpers/MetaFormats/Attributes/Param.php @@ -13,7 +13,7 @@ class Param { public Type $type; public string $paramName; - public array $validators; + public mixed $validators; public string $description; public bool $required; public bool $nullable; @@ -21,7 +21,7 @@ class Param /** * @param \App\Helpers\MetaFormats\Type $type The request parameter type (Post or Query). * @param string $name The name of the request parameter. - * @param array $validators An array of validators applied to the request parameter. + * @param mixed $validators A validator object or an array of validators applied to the request parameter. * @param string $description The description of the request parameter. * @param bool $required Whether the request parameter is required. * @param bool $nullable Whether the request parameter can be null. @@ -29,7 +29,7 @@ class Param public function __construct( Type $type, string $name, - array $validators, + mixed $validators, string $description = "", bool $required = true, bool $nullable = false, diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 6fc511e73..3b41d05f7 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -2,10 +2,12 @@ namespace App\Helpers\MetaFormats; +use App\Exceptions\InternalServerException; use App\Exceptions\InvalidArgumentException; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\Swagger\AnnotationParameterData; +use Exception; class RequestParamData { @@ -13,7 +15,7 @@ class RequestParamData public string $name; public string $description; public bool $required; - public array $validators; + public mixed $validators; public bool $nullable; public function __construct( @@ -21,7 +23,7 @@ public function __construct( string $name, string $description, bool $required, - array $validators = [], + mixed $validators = [], bool $nullable = false, ) { $this->type = $type; @@ -60,11 +62,27 @@ public function conformsToDefinition(mixed $value) return true; } - // use every provided validator - foreach ($this->validators as $validator) { - if (!$validator->validate($value)) { - throw new InvalidArgumentException($this->name); + ///TODO: check whether this works (test the internal exception as well) + // apply validators + // if an unexpected error is thrown, it is likely that the validator does not conform to the validator + // interface + try { + if (is_array($this->validators)) { + // use every provided validator + foreach ($this->validators as $validator) { + if (!$validator->validate($value)) { + throw new InvalidArgumentException($this->name); + } + } + } else { + if (!$this->validators->validate($value)) { + throw new InvalidArgumentException($this->name); + } } + } catch (Exception $e) { + throw new InternalServerException( + "The validator of parameter <{$this->name}> is corrupted. Parameter description: {$this->description}" + ); } return true; From 8630379a29a2b4fa2921bb1aa1f58952da2132c7 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Mon, 17 Feb 2025 13:31:19 +0100 Subject: [PATCH 036/103] bugfix: validators are now converted to arrays in attributes, description strings now escape quotes and $ characters --- .../NetteAnnotationConverter.php | 6 +++- .../Attributes/FormatParameterAttribute.php | 19 ++++++++-- app/helpers/MetaFormats/Attributes/Param.php | 13 ++----- app/helpers/MetaFormats/RequestParamData.php | 35 +++++++++---------- 4 files changed, 40 insertions(+), 33 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index 93dc5a821..11ccbb154 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -263,7 +263,11 @@ public static function convertRegexCapturesToParenthesesBuilder(array $annotatio $parenthesesBuilder->addValue(value: $validator); if (array_key_exists("description", $annotationParameters)) { - $parenthesesBuilder->addValue("\"{$annotationParameters["description"]}\""); + $description = $annotationParameters["description"]; + // escape all quotes and dollar signs + $description = str_replace("\"", "\\\"", $description); + $description = str_replace("$", "\\$", $description); + $parenthesesBuilder->addValue("\"{$description}\""); } if (array_key_exists("required", $annotationParameters)) { diff --git a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php index c354d1a73..c4c38fda9 100644 --- a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php +++ b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php @@ -2,6 +2,7 @@ namespace App\Helpers\MetaFormats\Attributes; +use App\Exceptions\InternalServerException; use App\Helpers\MetaFormats\Type; use Attribute; @@ -12,7 +13,7 @@ class FormatParameterAttribute { public Type $type; - public mixed $validators; + 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 @@ -27,15 +28,27 @@ class FormatParameterAttribute */ public function __construct( Type $type, - mixed $validators = [], + mixed $validators, string $description = "", bool $required = true, bool $nullable = false, ) { $this->type = $type; - $this->validators = $validators; $this->description = $description; $this->required = $required; $this->nullable = $nullable; + + // assign validators + if ($validators == null) { + throw new InternalServerException("Parameter Attribute validators are mandatory."); + } + if (!is_array($validators)) { + $this->validators = [ $validators ]; + } else { + if (count($validators) == 0) { + throw new InternalServerException("Parameter Attribute validators are mandatory."); + } + $this->validators = $validators; + } } } diff --git a/app/helpers/MetaFormats/Attributes/Param.php b/app/helpers/MetaFormats/Attributes/Param.php index 349c313e4..f0ef6362b 100644 --- a/app/helpers/MetaFormats/Attributes/Param.php +++ b/app/helpers/MetaFormats/Attributes/Param.php @@ -9,14 +9,9 @@ * Attribute used to annotate individual post or query parameters of endpoints. */ #[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)] -class Param +class Param extends FormatParameterAttribute { - public Type $type; public string $paramName; - public mixed $validators; - public string $description; - public bool $required; - public bool $nullable; /** * @param \App\Helpers\MetaFormats\Type $type The request parameter type (Post or Query). @@ -34,11 +29,7 @@ public function __construct( bool $required = true, bool $nullable = false, ) { - $this->type = $type; + parent::__construct($type, $validators, $description, $required, $nullable); $this->paramName = $name; - $this->validators = $validators; - $this->description = $description; - $this->required = $required; - $this->nullable = $nullable; } } diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 3b41d05f7..54638edde 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -15,7 +15,7 @@ class RequestParamData public string $name; public string $description; public bool $required; - public mixed $validators; + public array $validators; public bool $nullable; public function __construct( @@ -23,7 +23,7 @@ public function __construct( string $name, string $description, bool $required, - mixed $validators = [], + array $validators = [], bool $nullable = false, ) { $this->type = $type; @@ -67,21 +67,15 @@ public function conformsToDefinition(mixed $value) // if an unexpected error is thrown, it is likely that the validator does not conform to the validator // interface try { - if (is_array($this->validators)) { - // use every provided validator - foreach ($this->validators as $validator) { - if (!$validator->validate($value)) { - throw new InvalidArgumentException($this->name); - } - } - } else { - if (!$this->validators->validate($value)) { + // use every provided validator + foreach ($this->validators as $validator) { + if (!$validator->validate($value)) { throw new InvalidArgumentException($this->name); } } } catch (Exception $e) { throw new InternalServerException( - "The validator of parameter <{$this->name}> is corrupted. Parameter description: {$this->description}" + "The validator of parameter {$this->name} is corrupted. Parameter description: {$this->description}" ); } @@ -90,18 +84,23 @@ public function conformsToDefinition(mixed $value) private function hasValidators(): bool { - return count($this->validators) > 0; + if (is_array($this->validators)) { + return count($this->validators) > 0; + } + return $this->validators !== null; } public function toAnnotationParameterData() { + if (!$this->hasValidators()) { + throw new InternalServerException("No validator found for parameter {$this->name}, description: {$this->description}."); + } + $swaggerType = "string"; $nestedArraySwaggerType = null; - if ($this->hasValidators()) { - $swaggerType = $this->validators[0]::SWAGGER_TYPE; - if ($this->validators[0] instanceof VArray) { - $nestedArraySwaggerType = $this->validators[0]->getElementSwaggerType(); - } + $swaggerType = $this->validators[0]::SWAGGER_TYPE; + if ($this->validators[0] instanceof VArray) { + $nestedArraySwaggerType = $this->validators[0]->getElementSwaggerType(); } // retrieve the example value from the getExampleValue method if present From 85c40fe98a23e00979b3f53b7a23e2f29e7434f3 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Mon, 17 Feb 2025 15:15:59 +0100 Subject: [PATCH 037/103] path param values are no longer checked (there is no mechanism for that yet) --- app/V1Module/presenters/base/BasePresenter.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index adee900bf..e2295c038 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -240,6 +240,11 @@ 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); $formatInstanceArr[$param->name] = $paramValue; @@ -266,6 +271,11 @@ private function processParamsFormat(string $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 = $this->getValueFromParamData($requestParamData); if (!$formatInstance->checkedAssign($fieldName, $value)) { From 6d13d82c78f34e331bc7740a2c1f7eaefa9c1764 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Mon, 17 Feb 2025 15:56:29 +0100 Subject: [PATCH 038/103] added shorthands for attributes with a given type --- .../AnnotationToAttributeConverter.php | 8 +++-- .../NetteAnnotationConverter.php | 33 +++---------------- .../StandardAnnotationConverter.php | 8 ++--- .../AnnotationConversion/Utils.php | 24 ++++++++++++++ app/helpers/MetaFormats/Attributes/Path.php | 30 +++++++++++++++++ app/helpers/MetaFormats/Attributes/Post.php | 30 +++++++++++++++++ app/helpers/MetaFormats/Attributes/Query.php | 30 +++++++++++++++++ app/helpers/MetaFormats/MetaFormatHelper.php | 1 - 8 files changed, 126 insertions(+), 38 deletions(-) create mode 100644 app/helpers/MetaFormats/Attributes/Path.php create mode 100644 app/helpers/MetaFormats/Attributes/Post.php create mode 100644 app/helpers/MetaFormats/Attributes/Query.php diff --git a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php index 8bd600546..c1cbb9745 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php @@ -2,7 +2,6 @@ namespace App\Helpers\MetaFormats\AnnotationConversion; -use App\Helpers\MetaFormats\Attributes\Param; use App\Helpers\MetaFormats\Type; class AnnotationToAttributeConverter @@ -24,12 +23,15 @@ public static function convertFile(string $path): string $lines = []; $netteAttributeLinesCount = 0; $usingsAdded = false; - $paramAttributeClass = Utils::shortenClass(Param::class); + $paramAttributeClasses = Utils::getParamAttributeClassNames(); $paramTypeClass = Utils::shortenClass(Type::class); foreach (Utils::fileStringToLines($contentWithPlaceholders) as $line) { // detected the initial "use" block, add usings for new types if (!$usingsAdded && strlen($line) > 3 && substr($line, 0, 3) === "use") { - $lines[] = "use App\\Helpers\\MetaFormats\\Attributes\\{$paramAttributeClass};"; + // add usings for attributes + foreach ($paramAttributeClasses as $class) { + $lines[] = "use App\\Helpers\\MetaFormats\\Attributes\\{$class};"; + } $lines[] = "use App\\Helpers\\MetaFormats\\{$paramTypeClass};"; foreach (Utils::getValidatorNames() as $validator) { $lines[] = "use App\\Helpers\\MetaFormats\\Validators\\{$validator};"; diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index 11ccbb154..f7704395b 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -3,8 +3,6 @@ namespace App\Helpers\MetaFormats\AnnotationConversion; use App\Exceptions\InternalServerException; -use App\Helpers\MetaFormats\Attributes\Param; -use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; use App\Helpers\MetaFormats\Validators\VEmail; @@ -58,7 +56,7 @@ function ($matches) use (&$captures) { "captures" => $captures, ]; } - + /** * Converts regex parameter captures to an attribute string. * @param array $captures Regex parameter captures. @@ -66,9 +64,9 @@ function ($matches) use (&$captures) { */ public static function convertCapturesToAttributeString(array $captures) { - $paramAttributeClass = Utils::shortenClass(Param::class); - + $annotationParameters = NetteAnnotationConverter::convertCapturesToDictionary($captures); + $paramAttributeClass = Utils::getAttributeClassFromString($annotationParameters["type"]); $parenthesesBuilder = NetteAnnotationConverter::convertRegexCapturesToParenthesesBuilder($annotationParameters); $attributeLine = " #[{$paramAttributeClass}{$parenthesesBuilder->toString()}]"; // change to multiline if the line is too long @@ -208,7 +206,7 @@ private static function convertAnnotationValidationToValidatorString(string $val /** * Convers a parameter dictionary into an attribute string builder. * @param array $annotationParameters An associative array with a subset of the following keys: - * type, name, validation, description, required, nullable. + * name, validation, description, required, nullable. * @throws \App\Exceptions\InternalServerException * @return ParenthesesBuilder A string builder used to build the final attribute string. */ @@ -217,29 +215,6 @@ public static function convertRegexCapturesToParenthesesBuilder(array $annotatio // serialize the parameters to an attribute $parenthesesBuilder = new ParenthesesBuilder(); - // add type - if (!array_key_exists("type", $annotationParameters)) { - throw new InternalServerException("Missing type parameter."); - } - - $typeStr = $annotationParameters["type"]; - $paramTypeClass = Utils::shortenClass(Type::class); - $type = null; - switch ($typeStr) { - case "post": - $type = $paramTypeClass . "::Post"; - break; - case "query": - $type = $paramTypeClass . "::Query"; - break; - case "path": - $type = $paramTypeClass . "::Path"; - break; - default: - throw new InternalServerException("Unknown request type: $typeStr"); - } - $parenthesesBuilder->addValue($type); - // add name if (!array_key_exists("name", $annotationParameters)) { throw new InternalServerException("Missing name parameter."); diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php index 6e9fe1368..e3fbf1e9e 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -3,10 +3,8 @@ namespace App\Helpers\MetaFormats\AnnotationConversion; use App\Exceptions\InternalServerException; -use App\Helpers\MetaFormats\Attributes\Param; use App\Helpers\Swagger\AnnotationHelper; use App\Helpers\Swagger\AnnotationParameterData; -use App\V1Module\Presenters\BasePresenter; use ReflectionMethod; class StandardAnnotationConverter @@ -44,11 +42,11 @@ public static function convertStandardAnnotations(string $path): string $endpoint["startLine"] = $reflectionMethod->getStartLine() - 1; $endpoint["endLine"] = $reflectionMethod->getEndLine() - 1; } - + // sort endpoints based on position in the file (so that the file preprocessing can be done top-down) $startLines = array_column($endpoints, "startLine"); array_multisort($startLines, SORT_ASC, $endpoints); - + // get file lines $content = file_get_contents($path); $lines = Utils::fileStringToLines($content); @@ -159,7 +157,7 @@ private static function getAttributeLineFromMetadata(AnnotationParameterData $pa } $builder = NetteAnnotationConverter::convertRegexCapturesToParenthesesBuilder($data); - $paramAttributeClass = Utils::shortenClass(Param::class); + $paramAttributeClass = Utils::getAttributeClassFromString($data["type"]); $attributeLine = " #[{$paramAttributeClass}{$builder->toString()}]"; // change to multiline if the line is too long if (strlen($attributeLine) > 120) { diff --git a/app/helpers/MetaFormats/AnnotationConversion/Utils.php b/app/helpers/MetaFormats/AnnotationConversion/Utils.php index 06bead867..de35f0e98 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/Utils.php +++ b/app/helpers/MetaFormats/AnnotationConversion/Utils.php @@ -3,6 +3,9 @@ namespace App\Helpers\MetaFormats\AnnotationConversion; use App\Exceptions\InternalServerException; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; use App\V1Module\Presenters\BasePresenter; class Utils @@ -82,4 +85,25 @@ public static function getPresenterNamespace() $namespace = implode("\\", array_slice($namespaceTokens, 0, count($namespaceTokens) - 1)); return $namespace; } + + public static function getAttributeClassFromString(string $type) + { + switch ($type) { + case "post": + return self::shortenClass(Post::class); + case "query": + return self::shortenClass(Query::class); + case "path": + return self::shortenClass(Path::class); + } + } + + public static function getParamAttributeClassNames() + { + return [ + self::shortenClass(Post::class), + self::shortenClass(Query::class), + self::shortenClass(Path::class), + ]; + } } diff --git a/app/helpers/MetaFormats/Attributes/Path.php b/app/helpers/MetaFormats/Attributes/Path.php new file mode 100644 index 000000000..06d4c38fe --- /dev/null +++ b/app/helpers/MetaFormats/Attributes/Path.php @@ -0,0 +1,30 @@ + Date: Tue, 18 Feb 2025 16:38:24 +0100 Subject: [PATCH 039/103] code quality improvement --- .../presenters/base/BasePresenter.php | 20 ----- app/commands/MetaTester.php | 1 - .../NetteAnnotationConverter.php | 10 +-- .../StandardAnnotationConverter.php | 4 +- app/helpers/MetaFormats/FormatCache.php | 1 - .../FormatDefinitions/UserFormat.php | 23 +++--- app/helpers/MetaFormats/MetaFormatHelper.php | 25 +------ app/helpers/MetaFormats/RequestParamData.php | 6 +- app/helpers/Swagger/AnnotationHelper.php | 21 +----- app/model/view/MetaView.php | 74 ------------------- app/model/view/TestView.php | 20 ----- 11 files changed, 27 insertions(+), 178 deletions(-) delete mode 100644 app/model/view/TestView.php diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index e2295c038..20b09f5c9 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -347,26 +347,6 @@ private function getQueryField($param, $required = true) return $value; } - private function validateValue($param, $value, $validationRule, $msg = null) - { - foreach (["int", "integer"] as $rule) { - if ($validationRule === $rule || str_starts_with($validationRule, $rule . ":")) { - throw new LogicException("Validation rule '$validationRule' will not work for request parameters"); - } - } - - $value = Validators::preprocessValue($value, $validationRule); - if (Validators::is($value, $validationRule) === false) { - throw new InvalidArgumentException( - $param, - $msg ?? "The value '$value' does not match validation rule '$validationRule'" - . " - for more information check the documentation of Nette\\Utils\\Validators" - ); - } - - return $value; - } - protected function logUserAction($code = IResponse::S200_OK) { if ($this->getUser()->isLoggedIn()) { diff --git a/app/commands/MetaTester.php b/app/commands/MetaTester.php index c0c13fdf5..58681e58e 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaTester.php @@ -14,7 +14,6 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use App\Model\View\TestView; ///TODO: this command is debug only, delete it class MetaTester extends Command diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index f7704395b..7c8d0f529 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -64,7 +64,7 @@ function ($matches) use (&$captures) { */ public static function convertCapturesToAttributeString(array $captures) { - + $annotationParameters = NetteAnnotationConverter::convertCapturesToDictionary($captures); $paramAttributeClass = Utils::getAttributeClassFromString($annotationParameters["type"]); $parenthesesBuilder = NetteAnnotationConverter::convertRegexCapturesToParenthesesBuilder($annotationParameters); @@ -151,16 +151,16 @@ private static function convertAnnotationValidationToValidatorString(string $val } // type "255", exact match - if ($matches[2] === null) { + if ($matches[2] == null) { return "new {$stringValidator}({$matches[1]}, {$matches[1]})"; // type "1..255" - } elseif ($matches[1] !== null && $matches[3] !== null) { + } elseif ($matches[1] != null && $matches[3] !== null) { return "new {$stringValidator}({$matches[1]}, {$matches[3]})"; // type "..255" - } elseif ($matches[1] === null) { + } elseif ($matches[1] == null) { return "new {$stringValidator}(0, {$matches[3]})"; // type "1.." - } elseif ($matches[3] === null) { + } elseif ($matches[3] == null) { return "new {$stringValidator}({$matches[1]})"; } diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php index e3fbf1e9e..c09f44ade 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -79,8 +79,8 @@ public static function convertStandardAnnotations(string $path): string * @param array $endpoints Endpoint method metadata sorted by line number. * @param array $lines Lines of the file to be converted. * @throws \App\Exceptions\InternalServerException - * @return array} a list of dictionaries - * containing the new annotation lines and the end line of the original annotations. + * @return array A list of dictionaries containing the new annotation lines and the end line + * of the original annotations. */ private static function convertEndpointAnnotations(array $endpoints, array $lines): array { diff --git a/app/helpers/MetaFormats/FormatCache.php b/app/helpers/MetaFormats/FormatCache.php index 6403c3bab..d5990b3a2 100644 --- a/app/helpers/MetaFormats/FormatCache.php +++ b/app/helpers/MetaFormats/FormatCache.php @@ -9,7 +9,6 @@ class FormatCache private static ?array $formatToClassMap = null; private static ?array $classToFormatMap = null; private static ?array $formatToFieldFormatsMap = null; - private static ?array $validators = null; public static function getFormatToClassMap(): array { diff --git a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php index c79223b7f..2f7bc8e3c 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php @@ -6,41 +6,44 @@ use App\Helpers\MetaFormats\MetaFormat; use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute; use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VString; #[FormatAttribute(UserFormat::class)] class UserFormat extends MetaFormat { #[FormatAttribute("email")] - #[FormatParameterAttribute(type: Type::Post, description: "An email that will serve as a login name")] + #[FormatParameterAttribute(Type::Post, new VEmail(), "An email that will serve as a login name")] public string $email; - #[FormatParameterAttribute(type: Type::Post, description: "First name")] + #[FormatParameterAttribute(Type::Post, new VString(), "First name")] public string $firstName; - #[FormatParameterAttribute(type: Type::Post, description: "Last name", validators: [ new VString(2) ])] + #[FormatParameterAttribute(Type::Post, new VString(), "Last name", validators: [ new VString(2) ])] public string $lastName; - #[FormatParameterAttribute(type: Type::Post, description: "A password for authentication")] + #[FormatParameterAttribute(Type::Post, new VString(), "A password for authentication")] public string $password; - #[FormatParameterAttribute(type: Type::Post, description: "A password confirmation")] + #[FormatParameterAttribute(Type::Post, new VString(), "A password confirmation")] public string $passwordConfirm; - #[FormatParameterAttribute(type: Type::Post, description: "Identifier of the instance to register in")] + #[FormatParameterAttribute(Type::Post, new VString(), "Identifier of the instance to register in")] public string $instanceId; #[FormatParameterAttribute( - type: Type::Post, - description: "Titles that are placed before user name", + Type::Post, + new VString(), + "Titles that are placed before user name", required: false, nullable: true )] public ?string $titlesBeforeName; #[FormatParameterAttribute( - type: Type::Post, - description: "Titles that are placed after user name", + Type::Post, + new VString(), + "Titles that are placed after user name", required: false, nullable: true )] diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 2cbfc5832..778150fd5 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -16,27 +16,6 @@ class MetaFormatHelper private static string $formatDefinitionFolder = __DIR__ . '/FormatDefinitions'; private static string $formatDefinitionsNamespace = "App\\Helpers\\MetaFormats\\FormatDefinitions"; - private static function extractFormatData(array $annotations) - { - $filtered = AnnotationHelper::filterAnnotations($annotations, "@format"); - // there should either be one or none format declaration - if (count($filtered) == 0) { - return null; - } - if (count($filtered) > 1) { - ///TODO: throw exception - echo "Error in extractFormatData: Multiple format definitions.\n"; - return null; - } - - // sample: @format uuid - $annotation = $filtered[0]; - $tokens = explode(" ", $annotation); - $format = $tokens[1]; - - return $format; - } - /** * Checks all @checked_param annotations of a method and returns a map from parameter names to their formats. * @param string $className The name of the containing class. @@ -142,9 +121,9 @@ public static function debugGetAttributes( /** * Parses the format attributes of class fields and returns their metadata. * @param string $className The name of the class. - * @return array{format: string|null, type: string|null} with the field name as the key. + * @return array Returns a dictionary with the field name as the key and RequestParamData as the value. */ - public static function createNameToFieldDefinitionsMap(string $className) + public static function createNameToFieldDefinitionsMap(string $className): array { $class = new ReflectionClass($className); $fields = get_class_vars($className); diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 54638edde..a96772c5e 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -93,7 +93,9 @@ private function hasValidators(): bool public function toAnnotationParameterData() { if (!$this->hasValidators()) { - throw new InternalServerException("No validator found for parameter {$this->name}, description: {$this->description}."); + throw new InternalServerException( + "No validator found for parameter {$this->name}, description: {$this->description}." + ); } $swaggerType = "string"; @@ -105,7 +107,7 @@ public function toAnnotationParameterData() // retrieve the example value from the getExampleValue method if present $exampleValue = null; - if ($this->hasValidators() && method_exists(get_class($this->validators[0]), "getExampleValue")) { + if (method_exists(get_class($this->validators[0]), "getExampleValue")) { $exampleValue = $this->validators[0]->getExampleValue(); } diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index a766a8e2c..9251126ba 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -82,7 +82,7 @@ private static function extractAnnotationHttpMethod(array $annotations): HttpMet return null; } - private static function isDatatypeNullable(string $annotationType): bool + private static function isDatatypeNullable(mixed $annotationType): bool { // if the dataType is not specified (it is null), it means that the annotation is not // complete and defaults to a non nullable string @@ -194,25 +194,6 @@ private static function extractStandardAnnotationParams(array $annotations, stri return $params; } - /** - * Converts an array of assignment string to an associative array. - * @param array $expressions An array containing values in the following format: 'key="value"'. - * @return array Returns an associative array made from the string array. - */ - private static function stringArrayToAssociativeArray(array $expressions): array - { - $dict = []; - //sample: [ 'name="uiData"', 'validation="array|null"' ] - foreach ($expressions as $expression) { - $tokens = explode('="', $expression); - $name = $tokens[0]; - // remove the '"' at the end - $value = substr($tokens[1], 0, -1); - $dict[$name] = $value; - } - return $dict; - } - /** * Parses an annotation string and returns the lines as an array. * Lines not starting with '@' are assumed to be continuations of a parent line starting with @ (or the initial diff --git a/app/model/view/MetaView.php b/app/model/view/MetaView.php index 81c1c74e1..b4fdc535d 100644 --- a/app/model/view/MetaView.php +++ b/app/model/view/MetaView.php @@ -10,78 +10,4 @@ class MetaView { - /** - * Extracts the parameters and annotations of the calling function and converts the parameters to their target type. - * @return object[] Returns a map paramName=>targetTypeInstance, where the instances are filled with the - * data from the parameters. - */ - public function getTypedParams() - { - // extract function params of the caller - $backtrace = debug_backtrace()[1]; - $className = $backtrace['class']; - $methodName = $backtrace['function']; - // get param values - $paramsToValues = $this->getParamNamesToValuesMap($backtrace); - // get param format - $paramsToFormat = MetaFormatHelper::extractMethodCheckedParams($className, $methodName); - - // get all format definitions - $formats = MetaFormatHelper::getFormatDefinitions(); - var_dump($formats); - - $paramToTypedMap = []; - foreach ($paramsToValues as $paramName => $paramValue) { - - // the parameter name was not present in the annotations - if (!array_key_exists($paramName, $paramsToFormat)) { - throw new InternalServerException("Unknown method parameter format: $paramName\n"); - } - - $format = $paramsToFormat[$paramName]; - - // the format is not defined - if (!array_key_exists($format, $formats)) { - throw new InternalServerException("The format does not have a definition class: $format\n"); - } - - $targetClassName = $formats[$format]; - $classFormat = MetaFormatHelper::getClassFormats($targetClassName); - $obj = new $targetClassName(); - - // fill the new object with the param values - ///TODO: handle nested formated objects - foreach ($paramValue as $propertyName => $propertyValue) { - ///TODO: return 404 - // the property was not present in the class definition - if (!array_key_exists($propertyName, $classFormat)) { - echo "Error: unknown param: $paramName\n"; - return []; - } - - $obj->$propertyName = $propertyValue; - } - - $paramToTypedMap[$paramName] = $obj; - } - - return $paramToTypedMap; - } - - private function getParamNamesToValuesMap($backtrace): array - { - $className = $backtrace['class']; - $args = $backtrace['args']; - $methodName = $backtrace['function']; - - $class = new \ReflectionClass($className); - $method = $class->getMethod($methodName); - $params = array_map(fn($param) => $param->name, $method->getParameters()); - - $argMap = []; - for ($i = 0; $i < count($params); $i++) { - $argMap[$params[$i]] = $args[$i]; - } - return $argMap; - } } diff --git a/app/model/view/TestView.php b/app/model/view/TestView.php deleted file mode 100644 index 54eb0d8ea..000000000 --- a/app/model/view/TestView.php +++ /dev/null @@ -1,20 +0,0 @@ -getTypedParams(); - $formattedGroup = $params["group"]; - var_dump($formattedGroup); - - // $a = new GroupFormat(); - // $a->validate(); - } -} From 614864226062023bd4692ce65216dee83868426d Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Tue, 18 Feb 2025 16:48:24 +0100 Subject: [PATCH 040/103] removed duplicit validator parameter --- app/helpers/MetaFormats/FormatDefinitions/UserFormat.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php index 2f7bc8e3c..f44d654d3 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php @@ -19,7 +19,7 @@ class UserFormat extends MetaFormat #[FormatParameterAttribute(Type::Post, new VString(), "First name")] public string $firstName; - #[FormatParameterAttribute(Type::Post, new VString(), "Last name", validators: [ new VString(2) ])] + #[FormatParameterAttribute(Type::Post, new VString(), "Last name")] public string $lastName; #[FormatParameterAttribute(Type::Post, new VString(), "A password for authentication")] From 298866915553cceba5b2d223e74c4650adadbb20 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Tue, 18 Feb 2025 16:56:27 +0100 Subject: [PATCH 041/103] removed logging in the base presenter, removed format attribute and meta request from registration presenter --- app/V1Module/presenters/RegistrationPresenter.php | 3 +-- app/V1Module/presenters/base/BasePresenter.php | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/V1Module/presenters/RegistrationPresenter.php b/app/V1Module/presenters/RegistrationPresenter.php index 27aade7f9..971a36675 100644 --- a/app/V1Module/presenters/RegistrationPresenter.php +++ b/app/V1Module/presenters/RegistrationPresenter.php @@ -165,10 +165,9 @@ public function checkCreateAccount() * @throws WrongCredentialsException * @throws InvalidArgumentException */ - #[FormatAttribute(UserFormat::class)] public function actionCreateAccount() { - $req = $this->getMetaRequest(); + $req = $this->getRequest(); // check if the email is free $email = trim($req->getPost("email")); diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 20b09f5c9..b64686ea0 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -134,8 +134,6 @@ public function startup() Validators::init(); $this->processParams($actionReflection); - - $this->logger->log(var_export($this->getRequest(), true), ILogger::DEBUG); } protected function isRequestJson(): bool @@ -220,8 +218,6 @@ public function getMetaRequest(): MetaRequest | null private function processParams(ReflectionMethod $reflection) { - $this->logger->log(var_export(MetaFormatHelper::debugGetAttributes($reflection), true), ILogger::DEBUG); - // use a method specialized for formats if there is a format available $format = MetaFormatHelper::extractFormatFromAttribute($reflection); if ($format !== null) { @@ -249,7 +245,6 @@ private function processParamsLoose(array $paramData) $formatInstanceArr[$param->name] = $paramValue; // this throws when it does not conform - $this->logger->log(var_export($param, true), ILogger::DEBUG); $param->conformsToDefinition($paramValue); } From 042f2f318d9d16dd9d3cecce5fb1d33abb3e538e Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 12:34:08 +0100 Subject: [PATCH 042/103] @param annotations are removed after conversion --- .../StandardAnnotationConverter.php | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php index c09f44ade..75dd1feea 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -106,21 +106,42 @@ private static function convertEndpointAnnotations(array $endpoints, array $line throw new InternalServerException("Could not find annotation start line"); } + // get all annotation lines for the endoint $annotationLines = array_slice($lines, $annotationStartLine, $annotationEndLine - $annotationStartLine + 1); $params = $annotationData->getAllParams(); - /// attempt to remove param lines, but it is too complicated (handle missing param lines + multiline params) - // foreach ($params as $param) { - // // matches the line containing the parameter name with word boundaries - // $paramLineRegex = "/\\$\\b" . $param->name . "\\b/"; - // $lineIdx = -1; - // for ($i = 0; $i < count($annotationLines); $i++) { - // if (preg_match($paramLineRegex, $annotationLines[$i]) == 1) { - // $lineIdx = $i; - // break; - // } - // } - // } + foreach ($params as $param) { + // matches the line containing the parameter name with word boundaries + $paramLineRegex = "/\\$\\b" . $param->name . "\\b/"; + $lineIdx = -1; + for ($i = 0; $i < count($annotationLines); $i++) { + if (preg_match($paramLineRegex, $annotationLines[$i]) == 1) { + $lineIdx = $i; + break; + } + } + + // the endpoint is missing the annotation for the parameter, skip the parameter + if ($lineIdx == -1) { + continue; + } + + // length of the param annotation in lines + $paramAnnotationLength = 1; + // matches lines starting with an asterisks not continued by the @ symbol + $paramContinuationRegex = "/\h*\*\h+[^@]/"; + // find out how long the parameter annotation is + for ($i = $lineIdx + 1; $i < count($annotationLines); $i++) { + if (preg_match($paramContinuationRegex, $annotationLines[$i]) == 1) { + $paramAnnotationLength += 1; + } else { + break; + } + } + + // remove param annotations + array_splice($annotationLines, $lineIdx, $paramAnnotationLength); + } // crate an attribute from each parameter foreach ($params as $param) { From 8078b72ed1c8aaf299b9b11bf6612e9835a00238 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 12:37:33 +0100 Subject: [PATCH 043/103] converted endpoints --- .../AssignmentSolutionReviewsPresenter.php | 80 +++++-- .../AssignmentSolutionsPresenter.php | 51 +++-- .../presenters/AssignmentSolversPresenter.php | 24 +- .../presenters/AssignmentsPresenter.php | 162 ++++++++----- .../presenters/AsyncJobsPresenter.php | 31 ++- app/V1Module/presenters/BrokerPresenter.php | 12 + .../presenters/BrokerReportsPresenter.php | 20 +- app/V1Module/presenters/CommentsPresenter.php | 35 ++- app/V1Module/presenters/DefaultPresenter.php | 12 + .../presenters/EmailVerificationPresenter.php | 12 + app/V1Module/presenters/EmailsPresenter.php | 45 ++-- .../presenters/ExerciseFilesPresenter.php | 36 ++- .../presenters/ExercisesConfigPresenter.php | 82 ++++--- .../presenters/ExercisesPresenter.php | 155 +++++++------ .../presenters/ExtensionsPresenter.php | 19 +- .../presenters/ForgottenPasswordPresenter.php | 19 +- .../GroupExternalAttributesPresenter.php | 26 ++- .../presenters/GroupInvitationsPresenter.php | 26 ++- app/V1Module/presenters/GroupsPresenter.php | 213 ++++++++++-------- .../presenters/HardwareGroupsPresenter.php | 12 + .../presenters/InstancesPresenter.php | 49 ++-- app/V1Module/presenters/LoginPresenter.php | 30 ++- .../presenters/NotificationsPresenter.php | 49 ++-- .../presenters/PipelinesPresenter.php | 92 +++++--- .../presenters/PlagiarismPresenter.php | 68 ++++-- .../ReferenceExerciseSolutionsPresenter.php | 67 +++--- .../presenters/RegistrationPresenter.php | 72 +++--- .../RuntimeEnvironmentsPresenter.php | 12 + app/V1Module/presenters/SecurityPresenter.php | 16 +- .../presenters/ShadowAssignmentsPresenter.php | 88 +++++--- app/V1Module/presenters/SisPresenter.php | 52 +++-- .../SubmissionFailuresPresenter.php | 22 +- app/V1Module/presenters/SubmitPresenter.php | 45 ++-- .../presenters/UploadedFilesPresenter.php | 66 ++++-- .../presenters/UserCalendarsPresenter.php | 20 +- app/V1Module/presenters/UsersPresenter.php | 197 ++++++++++------ .../presenters/WorkerFilesPresenter.php | 22 +- 37 files changed, 1330 insertions(+), 709 deletions(-) diff --git a/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php b/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php index 5529b0c8b..f58d3f41e 100644 --- a/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php +++ b/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Exceptions\InternalServerException; use App\Exceptions\InvalidArgumentException; @@ -88,9 +100,9 @@ public function checkDefault(string $id) /** * Get detail of the solution and a list of review comments. * @GET - * @param string $id identifier of the solution * @throws InternalServerException */ + #[Path("id", new VString(), "identifier of the solution", required: true)] public function actionDefault(string $id) { $solution = $this->assignmentSolutions->findOrThrow($id); @@ -111,11 +123,10 @@ public function checkUpdate(string $id) /** * Update the state of the review process of the solution. * @POST - * @Param(type="post", name="close", validation="bool" - * description="If true, the review is closed. If false, the review is (re)opened.") - * @param string $id identifier of the solution * @throws InternalServerException */ + #[Post("close", new VBool(), "If true, the review is closed. If false, the review is (re)opened.")] + #[Path("id", new VString(), "identifier of the solution", required: true)] public function actionUpdate(string $id) { $solution = $this->assignmentSolutions->findOrThrow($id); @@ -182,9 +193,9 @@ public function checkRemove(string $id) /** * Update the state of the review process of the solution. * @DELETE - * @param string $id identifier of the solution * @throws InternalServerException */ + #[Path("id", new VString(), "identifier of the solution", required: true)] public function actionRemove(string $id) { $solution = $this->assignmentSolutions->findOrThrow($id); @@ -251,18 +262,29 @@ private function verifyCodeLocation(AssignmentSolution $solution, string $file, /** * Create a new comment within a review. * @POST - * @Param(type="post", name="text", validation="string:1..65535", required=true, description="The comment itself.") - * @Param(type="post", name="file", validation="string:0..256", required=true, - * description="Identification of the file to which the comment is related to.") - * @Param(type="post", name="line", validation="numericint", required=true, - * description="Line in the designated file to which the comment is related to.") - * @Param(type="post", name="issue", validation="bool", required=false, - * description="Whether the comment is an issue (expected to be resolved by the student)") - * @Param(type="post", name="suppressNotification", validation="bool", required=false, - * description="If true, no email notification will be sent (only applies when the review has been closed)") - * @param string $id identifier of the solution * @throws InternalServerException */ + #[Post("text", new VString(1, 65535), "The comment itself.", required: true)] + #[Post( + "file", + new VString(0, 256), + "Identification of the file to which the comment is related to.", + required: true, + )] + #[Post("line", new VInt(), "Line in the designated file to which the comment is related to.", required: true)] + #[Post( + "issue", + new VBool(), + "Whether the comment is an issue (expected to be resolved by the student)", + required: false, + )] + #[Post( + "suppressNotification", + new VBool(), + "If true, no email notification will be sent (only applies when the review has been closed)", + required: false, + )] + #[Path("id", new VString(), "identifier of the solution", required: true)] public function actionNewComment(string $id) { $solution = $this->assignmentSolutions->findOrThrow($id); @@ -320,15 +342,23 @@ public function checkEditComment(string $id, string $commentId) /** * Update existing comment within a review. * @POST - * @Param(type="post", name="text", validation="string:1..65535", required=true, description="The comment itself.") - * @Param(type="post", name="issue", validation="bool", required=false, - * description="Whether the comment is an issue (expected to be resolved by the student)") - * @Param(type="post", name="suppressNotification", validation="bool", required=false, - * description="If true, no email notification will be sent (only applies when the review has been closed)") - * @param string $id identifier of the solution - * @param string $commentId identifier of the review comment * @throws InternalServerException */ + #[Post("text", new VString(1, 65535), "The comment itself.", required: true)] + #[Post( + "issue", + new VBool(), + "Whether the comment is an issue (expected to be resolved by the student)", + required: false, + )] + #[Post( + "suppressNotification", + new VBool(), + "If true, no email notification will be sent (only applies when the review has been closed)", + required: false, + )] + #[Path("id", new VString(), "identifier of the solution", required: true)] + #[Path("commentId", new VString(), "identifier of the review comment", required: true)] public function actionEditComment(string $id, string $commentId) { $solution = $this->assignmentSolutions->findOrThrow($id); @@ -386,9 +416,9 @@ public function checkDeleteComment(string $id, string $commentId) /** * Remove one comment from a review. * @DELETE - * @param string $id identifier of the solution - * @param string $commentId identifier of the review comment */ + #[Path("id", new VString(), "identifier of the solution", required: true)] + #[Path("commentId", new VString(), "identifier of the review comment", required: true)] public function actionDeleteComment(string $id, string $commentId) { $comment = $this->reviewComments->findOrThrow($commentId); @@ -422,8 +452,8 @@ public function checkPending(string $id) * Return all solutions with pending reviews that given user teaches (is admin/supervisor in corresponding groups). * Along with that it returns all assignment entities of the corresponding solutions. * @GET - * @param string $id of the user whose pending reviews are listed */ + #[Path("id", new VString(), "of the user whose pending reviews are listed", required: true)] public function actionPending(string $id) { $user = $this->users->findOrThrow($id); diff --git a/app/V1Module/presenters/AssignmentSolutionsPresenter.php b/app/V1Module/presenters/AssignmentSolutionsPresenter.php index fff801893..7d7bd740a 100644 --- a/app/V1Module/presenters/AssignmentSolutionsPresenter.php +++ b/app/V1Module/presenters/AssignmentSolutionsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Exceptions\InternalServerException; use App\Exceptions\InvalidArgumentException; @@ -142,9 +154,9 @@ public function checkSolution(string $id) /** * Get information about solutions. * @GET - * @param string $id Identifier of the solution * @throws InternalServerException */ + #[Path("id", new VString(), "Identifier of the solution", required: true)] public function actionSolution(string $id) { $solution = $this->assignmentSolutions->findOrThrow($id); @@ -172,11 +184,11 @@ public function checkUpdateSolution(string $id) /** * Update details about the solution (note, etc...) * @POST - * @Param(type="post", name="note", validation="string:0..1024", description="A note by the author of the solution") - * @param string $id Identifier of the solution * @throws NotFoundException * @throws InternalServerException */ + #[Post("note", new VString(0, 1024), "A note by the author of the solution")] + #[Path("id", new VString(), "Identifier of the solution", required: true)] public function actionUpdateSolution(string $id) { $req = $this->getRequest(); @@ -198,9 +210,9 @@ public function checkDeleteSolution(string $id) /** * Delete assignment solution with given identification. * @DELETE - * @param string $id identifier of assignment solution * @throws ForbiddenRequestException */ + #[Path("id", new VString(), "identifier of assignment solution", required: true)] public function actionDeleteSolution(string $id) { $solution = $this->assignmentSolutions->findOrThrow($id); @@ -236,8 +248,8 @@ public function checkSubmissions(string $id) /** * Get list of all submissions of a solution * @GET - * @param string $id Identifier of the solution */ + #[Path("id", new VString(), "Identifier of the solution", required: true)] public function actionSubmissions(string $id) { $solution = $this->assignmentSolutions->findOrThrow($id); @@ -271,10 +283,10 @@ public function checkSubmission(string $submissionId) /** * Get information about the evaluation of a submission * @GET - * @param string $submissionId Identifier of the submission * @throws NotFoundException * @throws InternalServerException */ + #[Path("submissionId", new VString(), "Identifier of the submission", required: true)] public function actionSubmission(string $submissionId) { $submission = $this->assignmentSolutionSubmissions->findOrThrow($submissionId); @@ -301,8 +313,8 @@ public function checkDeleteSubmission(string $submissionId) /** * Remove the submission permanently * @DELETE - * @param string $submissionId Identifier of the submission */ + #[Path("submissionId", new VString(), "Identifier of the submission", required: true)] public function actionDeleteSubmission(string $submissionId) { $submission = $this->assignmentSolutionSubmissions->findOrThrow($submissionId); @@ -327,15 +339,13 @@ public function checkSetBonusPoints(string $id) * Set new amount of bonus points for a solution (and optionally points override) * Returns array of solution entities that has been changed by this. * @POST - * @Param(type="post", name="bonusPoints", validation="numericint", - * description="New amount of bonus points, can be negative number") - * @Param(type="post", name="overriddenPoints", required=false, - * description="Overrides points assigned to solution by the system") - * @param string $id Identifier of the solution * @throws NotFoundException * @throws InvalidArgumentException * @throws InvalidStateException */ + #[Post("bonusPoints", new VInt(), "New amount of bonus points, can be negative number")] + #[Post("overriddenPoints", new VString(), "Overrides points assigned to solution by the system", required: false)] + #[Path("id", new VString(), "Identifier of the solution", required: true)] public function actionSetBonusPoints(string $id) { $solution = $this->assignmentSolutions->findOrThrow($id); @@ -423,14 +433,13 @@ public function checkSetFlag(string $id, string $flag) /** * Set flag of the assignment solution. * @POST - * @param string $id identifier of the solution - * @param string $flag name of the flag which should to be changed - * @Param(type="post", name="value", required=true, validation=boolean, - * description="True or false which should be set to given flag name") * @throws NotFoundException * @throws \Nette\Application\AbortException * @throws \Exception */ + #[Post("value", new VBool(), "True or false which should be set to given flag name", required: true)] + #[Path("id", new VString(), "identifier of the solution", required: true)] + #[Path("flag", new VString(), "name of the flag which should to be changed", required: true)] public function actionSetFlag(string $id, string $flag) { $req = $this->getRequest(); @@ -546,12 +555,12 @@ public function checkDownloadSolutionArchive(string $id) /** * Download archive containing all solution files for particular solution. * @GET - * @param string $id of assignment solution * @throws ForbiddenRequestException * @throws NotFoundException * @throws \Nette\Application\BadRequestException * @throws \Nette\Application\AbortException */ + #[Path("id", new VString(), "of assignment solution", required: true)] public function actionDownloadSolutionArchive(string $id) { $solution = $this->assignmentSolutions->findOrThrow($id); @@ -573,10 +582,10 @@ public function checkFiles(string $id) /** * Get the list of submitted files of the solution. * @GET - * @param string $id of assignment solution * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Path("id", new VString(), "of assignment solution", required: true)] public function actionFiles(string $id) { $solution = $this->assignmentSolutions->findOrThrow($id)->getSolution(); @@ -594,11 +603,11 @@ public function checkDownloadResultArchive(string $submissionId) /** * Download result archive from backend for particular submission. * @GET - * @param string $submissionId * @throws NotFoundException * @throws InternalServerException * @throws \Nette\Application\AbortException */ + #[Path("submissionId", new VString(), required: true)] public function actionDownloadResultArchive(string $submissionId) { $submission = $this->assignmentSolutionSubmissions->findOrThrow($submissionId); @@ -628,10 +637,10 @@ public function checkEvaluationScoreConfig(string $submissionId) /** * Get score configuration associated with given submission evaluation * @GET - * @param string $submissionId Identifier of the submission * @throws NotFoundException * @throws InternalServerException */ + #[Path("submissionId", new VString(), "Identifier of the submission", required: true)] public function actionEvaluationScoreConfig(string $submissionId) { $submission = $this->assignmentSolutionSubmissions->findOrThrow($submissionId); @@ -655,8 +664,8 @@ public function checkReviewRequests(string $id) * (is admin/supervisor in corresponding groups). * Along with that it returns all assignment entities of the corresponding solutions. * @GET - * @param string $id of the user whose solutions with requested reviews are listed */ + #[Path("id", new VString(), "of the user whose solutions with requested reviews are listed", required: true)] public function actionReviewRequests(string $id) { $user = $this->users->findOrThrow($id); diff --git a/app/V1Module/presenters/AssignmentSolversPresenter.php b/app/V1Module/presenters/AssignmentSolversPresenter.php index aa5dc7a60..2ef74fce0 100644 --- a/app/V1Module/presenters/AssignmentSolversPresenter.php +++ b/app/V1Module/presenters/AssignmentSolversPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Model\Entity\AssignmentSolutionSubmission; use App\Model\Repository\Assignments; @@ -92,11 +104,15 @@ public function checkDefault(?string $assignmentId, ?string $groupId, ?string $u * Get a list of assignment solvers based on given parameters (assignment/group and solver user). * Either assignment or group ID must be set (group is ignored if assignment is set), user ID is optional. * @GET - * @Param(type="query", name="assignmentId", required=false, validation="string:36") - * @Param(type="query", name="groupId", required=false, validation="string:36", - * description="An alternative for assignment ID, selects all assignments from a group.") - * @Param(type="query", name="userId", required=false, validation="string:36") */ + #[Query("assignmentId", new VUuid(), required: false)] + #[Query( + "groupId", + new VUuid(), + "An alternative for assignment ID, selects all assignments from a group.", + required: false, + )] + #[Query("userId", new VUuid(), required: false)] public function actionDefault(?string $assignmentId, ?string $groupId, ?string $userId): void { $user = $userId ? $this->users->findOrThrow($userId) : null; diff --git a/app/V1Module/presenters/AssignmentsPresenter.php b/app/V1Module/presenters/AssignmentsPresenter.php index 369017438..6e0db06ff 100644 --- a/app/V1Module/presenters/AssignmentsPresenter.php +++ b/app/V1Module/presenters/AssignmentsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\InvalidArgumentException; @@ -177,8 +189,8 @@ public function checkDetail(string $id) /** * Get details of an assignment * @GET - * @param string $id Identifier of the assignment */ + #[Path("id", new VString(), "Identifier of the assignment", required: true)] public function actionDetail(string $id) { $this->sendSuccessResponse($this->assignmentViewFactory->getAssignment($this->assignments->findOrThrow($id))); @@ -195,53 +207,89 @@ public function checkUpdateDetail(string $id) /** * Update details of an assignment * @POST - * @Param(type="post", name="version", validation="numericint", description="Version of the edited assignment") - * @Param(type="post", name="isPublic", validation="bool", - * description="Is the assignment ready to be displayed to students?") - * @Param(type="post", name="localizedTexts", validation="array", description="A description of the assignment") - * @Param(type="post", name="firstDeadline", validation="timestamp", - * description="First deadline for submission of the assignment") - * @Param(type="post", name="maxPointsBeforeFirstDeadline", validation="numericint", - * description="A maximum of points that can be awarded for a submission before first deadline") - * @Param(type="post", name="submissionsCountLimit", validation="numericint", - * description="A maximum amount of submissions by a student for the assignment") - * @Param(type="post", name="solutionFilesLimit", validation="numericint|null", - * description="Maximal number of files in a solution being submitted") - * @Param(type="post", name="solutionSizeLimit", validation="numericint|null", - * description="Maximal size (bytes) of all files in a solution being submitted") - * @Param(type="post", name="allowSecondDeadline", validation="bool", - * description="Should there be a second deadline for students who didn't make the first one?") - * @Param(type="post", name="visibleFrom", validation="timestamp", required=false, - * description="Date from which this assignment will be visible to students") - * @Param(type="post", name="canViewLimitRatios", validation="bool", - * description="Can all users view ratio of theirs solution memory and time usages and assignment limits?") - * @Param(type="post", name="canViewMeasuredValues", validation="bool", - * description="Can all users view absolute memory and time values?") - * @Param(type="post", name="canViewJudgeStdout", validation="bool", - * description="Can all users view judge primary log (stdout) of theirs solution?") - * @Param(type="post", name="canViewJudgeStderr", validation="bool", - * description="Can all users view judge secondary log (stderr) of theirs solution?") - * @Param(type="post", name="secondDeadline", validation="timestamp", required=false, - * description="A second deadline for submission of the assignment (with different point award)") - * @Param(type="post", name="maxPointsBeforeSecondDeadline", validation="numericint", required=false, - * description="A maximum of points that can be awarded for a late submission") - * @Param(type="post", name="maxPointsDeadlineInterpolation", validation="bool", - * description="Use linear interpolation for max. points between 1st and 2nd deadline") - * @Param(type="post", name="isBonus", validation="bool", - * description="If true, points from this exercise will not be included in overall score of group") - * @Param(type="post", name="pointsPercentualThreshold", validation="numeric", required=false, - * description="A minimum percentage of points needed to gain point from assignment") - * @Param(type="post", name="disabledRuntimeEnvironmentIds", validation="list", required=false, - * description="Identifiers of runtime environments that should not be used for student submissions") - * @Param(type="post", name="sendNotification", required=false, validation="bool", - * description="If email notification (when assignment becomes public) should be sent") - * @Param(type="post", name="isExam", required=false, validation="bool", - * description="This assignemnt is dedicated for an exam (should be solved in exam mode)") - * @param string $id Identifier of the updated assignment * @throws BadRequestException * @throws InvalidArgumentException * @throws NotFoundException */ + #[Post("version", new VInt(), "Version of the edited assignment")] + #[Post("isPublic", new VBool(), "Is the assignment ready to be displayed to students?")] + #[Post("localizedTexts", new VArray(), "A description of the assignment")] + #[Post("firstDeadline", new VTimestamp(), "First deadline for submission of the assignment")] + #[Post( + "maxPointsBeforeFirstDeadline", + new VInt(), + "A maximum of points that can be awarded for a submission before first deadline", + )] + #[Post("submissionsCountLimit", new VInt(), "A maximum amount of submissions by a student for the assignment")] + #[Post("solutionFilesLimit", new VInt(), "Maximal number of files in a solution being submitted", nullable: true)] + #[Post( + "solutionSizeLimit", + new VInt(), + "Maximal size (bytes) of all files in a solution being submitted", + nullable: true, + )] + #[Post( + "allowSecondDeadline", + new VBool(), + "Should there be a second deadline for students who didn't make the first one?", + )] + #[Post( + "visibleFrom", + new VTimestamp(), + "Date from which this assignment will be visible to students", + required: false, + )] + #[Post( + "canViewLimitRatios", + new VBool(), + "Can all users view ratio of theirs solution memory and time usages and assignment limits?", + )] + #[Post("canViewMeasuredValues", new VBool(), "Can all users view absolute memory and time values?")] + #[Post("canViewJudgeStdout", new VBool(), "Can all users view judge primary log (stdout) of theirs solution?")] + #[Post("canViewJudgeStderr", new VBool(), "Can all users view judge secondary log (stderr) of theirs solution?")] + #[Post( + "secondDeadline", + new VTimestamp(), + "A second deadline for submission of the assignment (with different point award)", + required: false, + )] + #[Post( + "maxPointsBeforeSecondDeadline", + new VInt(), + "A maximum of points that can be awarded for a late submission", + required: false, + )] + #[Post( + "maxPointsDeadlineInterpolation", + new VBool(), + "Use linear interpolation for max. points between 1st and 2nd deadline", + )] + #[Post("isBonus", new VBool(), "If true, points from this exercise will not be included in overall score of group")] + #[Post( + "pointsPercentualThreshold", + new VFloat(), + "A minimum percentage of points needed to gain point from assignment", + required: false, + )] + #[Post( + "disabledRuntimeEnvironmentIds", + new VArray(), + "Identifiers of runtime environments that should not be used for student submissions", + required: false, + )] + #[Post( + "sendNotification", + new VBool(), + "If email notification (when assignment becomes public) should be sent", + required: false, + )] + #[Post( + "isExam", + new VBool(), + "This assignemnt is dedicated for an exam (should be solved in exam mode)", + required: false, + )] + #[Path("id", new VString(), "Identifier of the updated assignment", required: true)] public function actionUpdateDetail(string $id) { $assignment = $this->assignments->findOrThrow($id); @@ -471,10 +519,10 @@ public function checkValidate(string $id) /** * Check if the version of the assignment is up-to-date. * @POST - * @Param(type="post", name="version", validation="numericint", description="Version of the assignment.") - * @param string $id Identifier of the assignment * @throws ForbiddenRequestException */ + #[Post("version", new VInt(), "Version of the assignment.")] + #[Path("id", new VString(), "Identifier of the assignment", required: true)] public function actionValidate($id) { $assignment = $this->assignments->findOrThrow($id); @@ -492,13 +540,13 @@ public function actionValidate($id) /** * Assign an exercise to a group * @POST - * @Param(type="post", name="exerciseId", description="Identifier of the exercise") - * @Param(type="post", name="groupId", description="Identifier of the group") * @throws ForbiddenRequestException * @throws BadRequestException * @throws InvalidStateException * @throws NotFoundException */ + #[Post("exerciseId", new VString(), "Identifier of the exercise")] + #[Post("groupId", new VString(), "Identifier of the group")] public function actionCreate() { $req = $this->getRequest(); @@ -576,8 +624,8 @@ public function checkRemove(string $id) /** * Delete an assignment * @DELETE - * @param string $id Identifier of the assignment to be removed */ + #[Path("id", new VString(), "Identifier of the assignment to be removed", required: true)] public function actionRemove(string $id) { $this->assignments->remove($this->assignments->findOrThrow($id)); @@ -594,11 +642,11 @@ public function checkSyncWithExercise(string $id) /** * Update the assignment so that it matches with the current version of the exercise (limits, texts, etc.) - * @param string $id Identifier of the assignment that should be synchronized * @POST * @throws BadRequestException * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the assignment that should be synchronized", required: true)] public function actionSyncWithExercise($id) { $assignment = $this->assignments->findOrThrow($id); @@ -631,9 +679,9 @@ public function checkSolutions(string $id) /** * Get a list of solutions of all users for the assignment * @GET - * @param string $id Identifier of the assignment * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the assignment", required: true)] public function actionSolutions(string $id) { $assignment = $this->assignments->findOrThrow($id); @@ -665,9 +713,9 @@ public function checkUserSolutions(string $id, string $userId) /** * Get a list of solutions created by a user of an assignment * @GET - * @param string $id Identifier of the assignment - * @param string $userId Identifier of the user */ + #[Path("id", new VString(), "Identifier of the assignment", required: true)] + #[Path("userId", new VString(), "Identifier of the user", required: true)] public function actionUserSolutions(string $id, string $userId) { $assignment = $this->assignments->findOrThrow($id); @@ -708,10 +756,10 @@ public function checkBestSolution(string $id, string $userId) /** * Get the best solution by a user to an assignment * @GET - * @param string $id Identifier of the assignment - * @param string $userId Identifier of the user * @throws ForbiddenRequestException */ + #[Path("id", new VString(), "Identifier of the assignment", required: true)] + #[Path("userId", new VString(), "Identifier of the user", required: true)] public function actionBestSolution(string $id, string $userId) { $assignment = $this->assignments->findOrThrow($id); @@ -738,9 +786,9 @@ public function checkBestSolutions(string $id) /** * Get the best solutions to an assignment for all students in group. * @GET - * @param string $id Identifier of the assignment * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the assignment", required: true)] public function actionBestSolutions(string $id) { $assignment = $this->assignments->findOrThrow($id); @@ -782,11 +830,11 @@ public function checkDownloadBestSolutionsArchive(string $id) /** * Download the best solutions of an assignment for all students in group. * @GET - * @param string $id Identifier of the assignment * @throws NotFoundException * @throws \Nette\Application\AbortException * @throws \Nette\Application\BadRequestException */ + #[Path("id", new VString(), "Identifier of the assignment", required: true)] public function actionDownloadBestSolutionsArchive(string $id) { $assignment = $this->assignments->findOrThrow($id); diff --git a/app/V1Module/presenters/AsyncJobsPresenter.php b/app/V1Module/presenters/AsyncJobsPresenter.php index bd1d9ffff..fb20a2dc3 100644 --- a/app/V1Module/presenters/AsyncJobsPresenter.php +++ b/app/V1Module/presenters/AsyncJobsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Async\Dispatcher; use App\Async\Handler\PingAsyncJobHandler; use App\Model\Repository\Assignments; @@ -65,9 +77,9 @@ public function checkDefault(string $id) /** * Retrieves details about particular async job. * @GET - * @param string $id job identifier * @throws NotFoundException */ + #[Path("id", new VString(), "job identifier", required: true)] public function actionDefault(string $id) { $asyncJob = $this->asyncJobs->findOrThrow($id); @@ -84,10 +96,20 @@ public function checkList() /** * Retrieves details about async jobs that are either pending or were recently completed. * @GET - * @param int|null $ageThreshold Maximal time since completion (in seconds), null = only pending operations - * @param bool|null $includeScheduled If true, pending scheduled events will be listed as well * @throws BadRequestException */ + #[Query( + "ageThreshold", + new VInt(), + "Maximal time since completion (in seconds), null = only pending operations", + required: false, + )] + #[Query( + "includeScheduled", + new VBool(), + "If true, pending scheduled events will be listed as well", + required: false, + )] public function actionList(?int $ageThreshold, ?bool $includeScheduled) { if ($ageThreshold && $ageThreshold < 0) { @@ -134,9 +156,9 @@ public function checkAbort(string $id) /** * Retrieves details about particular async job. * @POST - * @param string $id job identifier * @throws NotFoundException */ + #[Path("id", new VString(), "job identifier", required: true)] public function actionAbort(string $id) { $this->asyncJobs->beginTransaction(); @@ -188,6 +210,7 @@ public function checkAssignmentJobs($id) * Get all pending async jobs related to a particular assignment. * @GET */ + #[Path("id", new VString(), required: true)] public function actionAssignmentJobs($id) { $asyncJobs = $this->asyncJobs->findAssignmentJobs($id); diff --git a/app/V1Module/presenters/BrokerPresenter.php b/app/V1Module/presenters/BrokerPresenter.php index b208738c4..ab0737641 100644 --- a/app/V1Module/presenters/BrokerPresenter.php +++ b/app/V1Module/presenters/BrokerPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\InvalidStateException; use App\Helpers\BrokerProxy; diff --git a/app/V1Module/presenters/BrokerReportsPresenter.php b/app/V1Module/presenters/BrokerReportsPresenter.php index fc4cd4e86..7d17cf425 100644 --- a/app/V1Module/presenters/BrokerReportsPresenter.php +++ b/app/V1Module/presenters/BrokerReportsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\HttpBasicAuthException; use App\Exceptions\InternalServerException; use App\Exceptions\InvalidStateException; @@ -167,13 +179,13 @@ private function processJobFailure(JobId $job) /** * Update the status of a job (meant to be called by the backend) * @POST - * @Param(name="status", type="post", description="The new status of the job") - * @Param(name="message", type="post", required=false, description="A textual explanation of the status change") - * @param string $jobId Identifier of the job whose status is being reported * @throws InternalServerException * @throws NotFoundException * @throws InvalidStateException */ + #[Post("status", new VString(), "The new status of the job")] + #[Post("message", new VString(), "A textual explanation of the status change", required: false)] + #[Path("jobId", new VString(), "Identifier of the job whose status is being reported", required: true)] public function actionJobStatus($jobId) { $status = $this->getRequest()->getPost("status"); @@ -196,9 +208,9 @@ public function actionJobStatus($jobId) /** * Announce a backend error that is not related to any job (meant to be called by the backend) * @POST - * @Param(name="message", type="post", description="A textual description of the error") * @throws InternalServerException */ + #[Post("message", new VString(), "A textual description of the error")] public function actionError() { $req = $this->getRequest(); diff --git a/app/V1Module/presenters/CommentsPresenter.php b/app/V1Module/presenters/CommentsPresenter.php index d1bb8b774..b861ed411 100644 --- a/app/V1Module/presenters/CommentsPresenter.php +++ b/app/V1Module/presenters/CommentsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\NotFoundException; use App\Helpers\Notifications\SolutionCommentsEmailsSender; @@ -95,9 +107,9 @@ public function checkDefault($id) /** * Get a comment thread * @GET - * @param string $id Identifier of the comment thread * @throws ForbiddenRequestException */ + #[Path("id", new VString(), "Identifier of the comment thread", required: true)] public function actionDefault($id) { $thread = $this->findThreadOrCreateIt($id); @@ -123,12 +135,11 @@ public function checkAddComment(string $id) /** * Add a comment to a thread * @POST - * @Param(type="post", name="text", validation="string:1..65535", description="Text of the comment") - * @Param(type="post", name="isPrivate", validation="bool", required=false, - * description="True if the comment is private") - * @param string $id Identifier of the comment thread * @throws ForbiddenRequestException */ + #[Post("text", new VString(1, 65535), "Text of the comment")] + #[Post("isPrivate", new VBool(), "True if the comment is private", required: false)] + #[Path("id", new VString(), "Identifier of the comment thread", required: true)] public function actionAddComment(string $id) { $thread = $this->findThreadOrCreateIt($id); @@ -178,10 +189,10 @@ public function checkTogglePrivate(string $threadId, string $commentId) * Make a private comment public or vice versa * @DEPRECATED * @POST - * @param string $threadId Identifier of the comment thread - * @param string $commentId Identifier of the comment * @throws NotFoundException */ + #[Path("threadId", new VString(), "Identifier of the comment thread", required: true)] + #[Path("commentId", new VString(), "Identifier of the comment", required: true)] public function actionTogglePrivate(string $threadId, string $commentId) { /** @var Comment $comment */ @@ -211,11 +222,11 @@ public function checkSetPrivate(string $threadId, string $commentId) /** * Set the private flag of a comment * @POST - * @param string $threadId Identifier of the comment thread - * @param string $commentId Identifier of the comment - * @Param(type="post", name="isPrivate", validation="bool", description="True if the comment is private") * @throws NotFoundException */ + #[Post("isPrivate", new VBool(), "True if the comment is private")] + #[Path("threadId", new VString(), "Identifier of the comment thread", required: true)] + #[Path("commentId", new VString(), "Identifier of the comment", required: true)] public function actionSetPrivate(string $threadId, string $commentId) { /** @var Comment $comment */ @@ -248,11 +259,11 @@ public function checkDelete(string $threadId, string $commentId) /** * Delete a comment * @DELETE - * @param string $threadId Identifier of the comment thread - * @param string $commentId Identifier of the comment * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Path("threadId", new VString(), "Identifier of the comment thread", required: true)] + #[Path("commentId", new VString(), "Identifier of the comment", required: true)] public function actionDelete(string $threadId, string $commentId) { /** @var Comment $comment */ diff --git a/app/V1Module/presenters/DefaultPresenter.php b/app/V1Module/presenters/DefaultPresenter.php index 39a4427b7..d8be4c176 100644 --- a/app/V1Module/presenters/DefaultPresenter.php +++ b/app/V1Module/presenters/DefaultPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Helpers\ApiConfig; class DefaultPresenter extends BasePresenter diff --git a/app/V1Module/presenters/EmailVerificationPresenter.php b/app/V1Module/presenters/EmailVerificationPresenter.php index 8448378d8..fad20d0eb 100644 --- a/app/V1Module/presenters/EmailVerificationPresenter.php +++ b/app/V1Module/presenters/EmailVerificationPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Helpers\EmailVerificationHelper; use App\Security\Identity; diff --git a/app/V1Module/presenters/EmailsPresenter.php b/app/V1Module/presenters/EmailsPresenter.php index 3736dcdd9..c5925b0d8 100644 --- a/app/V1Module/presenters/EmailsPresenter.php +++ b/app/V1Module/presenters/EmailsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\NotFoundException; use App\Helpers\EmailHelper; @@ -55,10 +67,9 @@ public function checkDefault() /** * Sends an email with provided subject and message to all ReCodEx users. * @POST - * @Param(type="post", name="subject", validation="string:1..", description="Subject for the soon to be sent email") - * @Param(type="post", name="message", validation="string:1..", - * description="Message which will be sent, can be html code") */ + #[Post("subject", new VString(1), "Subject for the soon to be sent email")] + #[Post("message", new VString(1), "Message which will be sent, can be html code")] public function actionDefault() { $users = $this->users->findAll(); @@ -86,10 +97,9 @@ public function checkSendToSupervisors() /** * Sends an email with provided subject and message to all supervisors and superadmins. * @POST - * @Param(type="post", name="subject", validation="string:1..", description="Subject for the soon to be sent email") - * @Param(type="post", name="message", validation="string:1..", - * description="Message which will be sent, can be html code") */ + #[Post("subject", new VString(1), "Subject for the soon to be sent email")] + #[Post("message", new VString(1), "Message which will be sent, can be html code")] public function actionSendToSupervisors() { $supervisors = $this->users->findByRoles( @@ -123,10 +133,9 @@ public function checkSendToRegularUsers() /** * Sends an email with provided subject and message to all regular users. * @POST - * @Param(type="post", name="subject", validation="string:1..", description="Subject for the soon to be sent email") - * @Param(type="post", name="message", validation="string:1..", - * description="Message which will be sent, can be html code") */ + #[Post("subject", new VString(1), "Subject for the soon to be sent email")] + #[Post("message", new VString(1), "Message which will be sent, can be html code")] public function actionSendToRegularUsers() { $users = $this->users->findByRoles(Roles::STUDENT_ROLE, Roles::SUPERVISOR_STUDENT_ROLE); @@ -156,20 +165,16 @@ public function checkSendToGroupMembers(string $groupId) * Sends an email with provided subject and message to regular members of * given group and optionally to supervisors and admins. * @POST - * @param string $groupId - * @Param(type="post", name="toSupervisors", validation="bool", required=false, - * description="If true, then the mail will be sent to supervisors") - * @Param(type="post", name="toAdmins", validation="bool", required=false, - * description="If the mail should be sent also to primary admins") - * @Param(type="post", name="toObservers", validation="bool", required=false, - * description="If the mail should be sent also to observers") - * @Param(type="post", name="toMe", validation="bool", description="User wants to also receive an email") - * @Param(type="post", name="subject", validation="string:1..", description="Subject for the soon to be sent email") - * @Param(type="post", name="message", validation="string:1..", - * description="Message which will be sent, can be html code") * @throws NotFoundException * @throws ForbiddenRequestException */ + #[Post("toSupervisors", new VBool(), "If true, then the mail will be sent to supervisors", required: false)] + #[Post("toAdmins", new VBool(), "If the mail should be sent also to primary admins", required: false)] + #[Post("toObservers", new VBool(), "If the mail should be sent also to observers", required: false)] + #[Post("toMe", new VBool(), "User wants to also receive an email")] + #[Post("subject", new VString(1), "Subject for the soon to be sent email")] + #[Post("message", new VString(1), "Message which will be sent, can be html code")] + #[Path("groupId", new VString(), required: true)] public function actionSendToGroupMembers(string $groupId) { $user = $this->getCurrentUser(); diff --git a/app/V1Module/presenters/ExerciseFilesPresenter.php b/app/V1Module/presenters/ExerciseFilesPresenter.php index 7ec04d81c..39389360c 100644 --- a/app/V1Module/presenters/ExerciseFilesPresenter.php +++ b/app/V1Module/presenters/ExerciseFilesPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\InvalidArgumentException; use App\Exceptions\NotFoundException; @@ -85,12 +97,12 @@ public function checkUploadSupplementaryFiles(string $id) /** * Associate supplementary files with an exercise and upload them to remote file server * @POST - * @Param(type="post", name="files", description="Identifiers of supplementary files") - * @param string $id identification of exercise * @throws ForbiddenRequestException * @throws InvalidArgumentException * @throws SubmissionFailedException */ + #[Post("files", new VString(), "Identifiers of supplementary files")] + #[Path("id", new VString(), "identification of exercise", required: true)] public function actionUploadSupplementaryFiles(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -170,8 +182,8 @@ public function checkGetSupplementaryFiles(string $id) /** * Get list of all supplementary files for an exercise * @GET - * @param string $id identification of exercise */ + #[Path("id", new VString(), "identification of exercise", required: true)] public function actionGetSupplementaryFiles(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -189,10 +201,10 @@ public function checkDeleteSupplementaryFile(string $id, string $fileId) /** * Delete supplementary exercise file with given id * @DELETE - * @param string $id identification of exercise - * @param string $fileId identification of file * @throws ForbiddenRequestException */ + #[Path("id", new VString(), "identification of exercise", required: true)] + #[Path("fileId", new VString(), "identification of file", required: true)] public function actionDeleteSupplementaryFile(string $id, string $fileId) { $exercise = $this->exercises->findOrThrow($id); @@ -219,12 +231,12 @@ public function checkDownloadSupplementaryFilesArchive(string $id) /** * Download archive containing all supplementary files for exercise. * @GET - * @param string $id of exercise * @throws ForbiddenRequestException * @throws NotFoundException * @throws \Nette\Application\BadRequestException * @throws \Nette\Application\AbortException */ + #[Path("id", new VString(), "of exercise", required: true)] public function actionDownloadSupplementaryFilesArchive(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -248,10 +260,10 @@ public function checkUploadAttachmentFiles(string $id) /** * Associate attachment exercise files with an exercise * @POST - * @Param(type="post", name="files", description="Identifiers of attachment files") - * @param string $id identification of exercise * @throws ForbiddenRequestException */ + #[Post("files", new VString(), "Identifiers of attachment files")] + #[Path("id", new VString(), "identification of exercise", required: true)] public function actionUploadAttachmentFiles(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -304,9 +316,9 @@ public function checkGetAttachmentFiles(string $id) /** * Get a list of all attachment files for an exercise * @GET - * @param string $id identification of exercise * @throws ForbiddenRequestException */ + #[Path("id", new VString(), "identification of exercise", required: true)] public function actionGetAttachmentFiles(string $id) { /** @var Exercise $exercise */ @@ -326,11 +338,11 @@ public function checkDeleteAttachmentFile(string $id, string $fileId) /** * Delete attachment exercise file with given id * @DELETE - * @param string $id identification of exercise - * @param string $fileId identification of file * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Path("id", new VString(), "identification of exercise", required: true)] + #[Path("fileId", new VString(), "identification of file", required: true)] public function actionDeleteAttachmentFile(string $id, string $fileId) { $exercise = $this->exercises->findOrThrow($id); @@ -356,11 +368,11 @@ public function checkDownloadAttachmentFilesArchive(string $id) /** * Download archive containing all attachment files for exercise. * @GET - * @param string $id of exercise * @throws NotFoundException * @throws \Nette\Application\BadRequestException * @throws \Nette\Application\AbortException */ + #[Path("id", new VString(), "of exercise", required: true)] public function actionDownloadAttachmentFilesArchive(string $id) { $exercise = $this->exercises->findOrThrow($id); diff --git a/app/V1Module/presenters/ExercisesConfigPresenter.php b/app/V1Module/presenters/ExercisesConfigPresenter.php index 167fe02b7..7b636b5b5 100644 --- a/app/V1Module/presenters/ExercisesConfigPresenter.php +++ b/app/V1Module/presenters/ExercisesConfigPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ApiException; use App\Exceptions\ExerciseCompilationException; use App\Exceptions\ExerciseConfigException; @@ -159,9 +171,9 @@ private function getEnvironmentConfigs(Exercise $exercise) /** * Get runtime configurations for exercise. * @GET - * @param string $id Identifier of the exercise * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionGetEnvironmentConfigs(string $id) { /** @var Exercise $exercise */ @@ -183,14 +195,13 @@ public function checkUpdateEnvironmentConfigs(string $id) * Change runtime configuration of exercise. * Configurations can be added or deleted here, based on what is provided in arguments. * @POST - * @param string $id identification of exercise - * @Param(type="post", name="environmentConfigs", validation="array", - * description="Environment configurations for the exercise") * @throws ForbiddenRequestException * @throws InvalidArgumentException * @throws ExerciseConfigException * @throws NotFoundException */ + #[Post("environmentConfigs", new VArray(), "Environment configurations for the exercise")] + #[Path("id", new VString(), "identification of exercise", required: true)] public function actionUpdateEnvironmentConfigs(string $id) { /** @var Exercise $exercise */ @@ -269,10 +280,10 @@ public function checkGetConfiguration(string $id) /** * Get a basic exercise high level configuration. * @GET - * @param string $id Identifier of the exercise * @throws NotFoundException * @throws ExerciseConfigException */ + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionGetConfiguration(string $id) { /** @var Exercise $exercise */ @@ -302,15 +313,14 @@ public function checkSetConfiguration(string $id) /** * Set basic exercise configuration * @POST - * @Param(type="post", name="config", validation="array", - * description="A list of basic high level exercise configuration") - * @param string $id Identifier of the exercise * @throws ExerciseConfigException * @throws ForbiddenRequestException * @throws NotFoundException * @throws ApiException * @throws ParseException */ + #[Post("config", new VArray(), "A list of basic high level exercise configuration")] + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionSetConfiguration(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -359,14 +369,12 @@ public function checkGetVariablesForExerciseConfig(string $id) * Get variables for exercise configuration which are derived from given * pipelines and runtime environment. * @POST - * @param string $id Identifier of the exercise - * @Param(type="post", name="runtimeEnvironmentId", validation="string:1..", required=false, - * description="Environment identifier") - * @Param(type="post", name="pipelinesIds", validation="array", - * description="Identifiers of selected pipelines for one test") * @throws NotFoundException * @throws ExerciseConfigException */ + #[Post("runtimeEnvironmentId", new VString(1), "Environment identifier", required: false)] + #[Post("pipelinesIds", new VArray(), "Identifiers of selected pipelines for one test")] + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionGetVariablesForExerciseConfig(string $id) { // get request data @@ -406,13 +414,13 @@ public function checkGetHardwareGroupLimits(string $id, string $runtimeEnvironme * Get a description of resource limits for an exercise for given hwgroup. * @DEPRECATED * @GET - * @param string $id Identifier of the exercise - * @param string $runtimeEnvironmentId - * @param string $hwGroupId * @throws ForbiddenRequestException * @throws NotFoundException * @throws ExerciseConfigException */ + #[Path("id", new VString(), "Identifier of the exercise", required: true)] + #[Path("runtimeEnvironmentId", new VString(), required: true)] + #[Path("hwGroupId", new VString(), required: true)] public function actionGetHardwareGroupLimits(string $id, string $runtimeEnvironmentId, string $hwGroupId) { /** @var Exercise $exercise */ @@ -451,11 +459,6 @@ public function checkSetHardwareGroupLimits(string $id, string $runtimeEnvironme * Set resource limits for an exercise for given hwgroup. * @DEPRECATED * @POST - * @Param(type="post", name="limits", validation="array", - * description="A list of resource limits for the given environment and hardware group") - * @param string $id Identifier of the exercise - * @param string $runtimeEnvironmentId - * @param string $hwGroupId * @throws ApiException * @throws ExerciseConfigException * @throws ForbiddenRequestException @@ -463,6 +466,10 @@ public function checkSetHardwareGroupLimits(string $id, string $runtimeEnvironme * @throws ParseException * @throws ExerciseCompilationException */ + #[Post("limits", new VArray(), "A list of resource limits for the given environment and hardware group")] + #[Path("id", new VString(), "Identifier of the exercise", required: true)] + #[Path("runtimeEnvironmentId", new VString(), required: true)] + #[Path("hwGroupId", new VString(), required: true)] public function actionSetHardwareGroupLimits(string $id, string $runtimeEnvironmentId, string $hwGroupId) { /** @var Exercise $exercise */ @@ -522,11 +529,11 @@ public function checkRemoveHardwareGroupLimits(string $id, string $runtimeEnviro * Remove resource limits of given hwgroup from an exercise. * @DEPRECATED * @DELETE - * @param string $id Identifier of the exercise - * @param string $runtimeEnvironmentId - * @param string $hwGroupId * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the exercise", required: true)] + #[Path("runtimeEnvironmentId", new VString(), required: true)] + #[Path("hwGroupId", new VString(), required: true)] public function actionRemoveHardwareGroupLimits(string $id, string $runtimeEnvironmentId, string $hwGroupId) { /** @var Exercise $exercise */ @@ -565,10 +572,10 @@ public function checkGetLimits(string $id) /** * Get a description of resource limits for given exercise (all hwgroups all environments). * @GET - * @param string $id Identifier of the exercise * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionGetLimits(string $id) { /** @var Exercise $exercise */ @@ -605,13 +612,12 @@ public function checkSetLimits(string $id) * If limits for particular hwGroup or environment are not posted, no change occurs. * If limits for particular hwGroup or environment are posted as null, they are removed. * @POST - * @Param(type="post", name="limits", validation="array", - * description="A list of resource limits in the same format as getLimits endpoint yields.") - * @param string $id Identifier of the exercise * @throws ForbiddenRequestException * @throws NotFoundException * @throws ExerciseConfigException */ + #[Post("limits", new VArray(), "A list of resource limits in the same format as getLimits endpoint yields.")] + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionSetLimits(string $id) { /** @var Exercise $exercise */ @@ -669,8 +675,8 @@ public function checkGetScoreConfig(string $id) /** * Get score configuration for exercise based on given identification. * @GET - * @param string $id Identifier of the exercise */ + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionGetScoreConfig(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -690,12 +696,15 @@ public function checkSetScoreConfig(string $id) /** * Set score configuration for exercise. * @POST - * @Param(type="post", name="scoreCalculator", validation="string", description="ID of the score calculator") - * @Param(type="post", name="scoreConfig", - * description="A configuration of the score calculator (the format depends on the calculator type)") - * @param string $id Identifier of the exercise * @throws ExerciseConfigException */ + #[Post("scoreCalculator", new VString(), "ID of the score calculator")] + #[Post( + "scoreConfig", + new VString(), + "A configuration of the score calculator (the format depends on the calculator type)", + )] + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionSetScoreConfig(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -733,8 +742,8 @@ public function checkGetTests(string $id) /** * Get tests for exercise based on given identification. * @GET - * @param string $id Identifier of the exercise */ + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionGetTests(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -754,13 +763,12 @@ public function checkSetTests(string $id) /** * Set tests for exercise based on given identification. * @POST - * @param string $id Identifier of the exercise - * @Param(type="post", name="tests", validation="array", - * description="An array of tests which will belong to exercise") * @throws ForbiddenRequestException * @throws InvalidArgumentException * @throws ExerciseConfigException */ + #[Post("tests", new VArray(), "An array of tests which will belong to exercise")] + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionSetTests(string $id) { $exercise = $this->exercises->findOrThrow($id); diff --git a/app/V1Module/presenters/ExercisesPresenter.php b/app/V1Module/presenters/ExercisesPresenter.php index 309600405..6d81431ff 100644 --- a/app/V1Module/presenters/ExercisesPresenter.php +++ b/app/V1Module/presenters/ExercisesPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ApiException; use App\Exceptions\BadRequestException; use App\Exceptions\ForbiddenRequestException; @@ -184,12 +196,22 @@ public function checkDefault() * Get a list of all exercises matching given filters in given pagination rage. * The result conforms to pagination protocol. * @GET - * @param int $offset Index of the first result. - * @param int|null $limit Maximal number of results returned. - * @param string|null $orderBy Name of the column (column concept). The '!' prefix indicate descending order. - * @param array|null $filters Named filters that prune the result. - * @param string|null $locale Currently set locale (used to augment order by clause if necessary), */ + #[Query("offset", new VInt(), "Index of the first result.", required: false)] + #[Query("limit", new VInt(), "Maximal number of results returned.", required: false)] + #[Query( + "orderBy", + new VString(), + "Name of the column (column concept). The '!' prefix indicate descending order.", + required: false, + )] + #[Query("filters", new VArray(), "Named filters that prune the result.", required: false)] + #[Query( + "locale", + new VString(), + "Currently set locale (used to augment order by clause if necessary),", + required: false, + )] public function actionDefault( int $offset = 0, int $limit = null, @@ -238,9 +260,9 @@ public function checkAuthors() /** * List authors of all exercises, possibly filtered by a group in which the exercises appear. * @GET - * @param string $instanceId Id of an instance from which the authors are listed. - * @param string|null $groupId A group where the relevant exercises can be seen (assigned). */ + #[Query("instanceId", new VString(), "Id of an instance from which the authors are listed.", required: false)] + #[Query("groupId", new VString(), "A group where the relevant exercises can be seen (assigned).", required: false)] public function actionAuthors(string $instanceId = null, string $groupId = null) { $authors = $this->exercises->getAuthors($instanceId, $groupId, $this->groups); @@ -257,8 +279,8 @@ public function checkListByIds() /** * Get a list of exercises based on given ids. * @POST - * @Param(type="post", name="ids", validation="array", description="Identifications of exercises") */ + #[Post("ids", new VArray(), "Identifications of exercises")] public function actionListByIds() { $exercises = $this->exercises->findByIds($this->getRequest()->getPost("ids")); @@ -283,8 +305,8 @@ public function checkDetail(string $id) /** * Get details of an exercise * @GET - * @param string $id identification of exercise */ + #[Path("id", new VString(), "identification of exercise", required: true)] public function actionDetail(string $id) { /** @var Exercise $exercise */ @@ -304,29 +326,37 @@ public function checkUpdateDetail(string $id) /** * Update detail of an exercise * @POST - * @param string $id identification of exercise * @throws BadRequestException * @throws ForbiddenRequestException * @throws InvalidArgumentException - * @Param(type="post", name="version", validation="numericint", description="Version of the edited exercise") - * @Param(type="post", name="difficulty", - * description="Difficulty of an exercise, should be one of 'easy', 'medium' or 'hard'") - * @Param(type="post", name="localizedTexts", validation="array", description="A description of the exercise") - * @Param(type="post", name="isPublic", validation="bool", required=false, - * description="Exercise can be public or private") - * @Param(type="post", name="isLocked", validation="bool", required=false, - * description="If true, the exercise cannot be assigned") - * @Param(type="post", name="configurationType", validation="string", required=false, - * description="Identifies the way the evaluation of the exercise is configured") - * @Param(type="post", name="solutionFilesLimit", validation="numericint|null", - * description="Maximal number of files in a solution being submitted (default for assignments)") - * @Param(type="post", name="solutionSizeLimit", validation="numericint|null", - * description="Maximal size (bytes) of all files in a solution being submitted (default for assignments)") - * @Param(type="post", name="mergeJudgeLogs", validation="bool", - * description="If true, judge stderr will be merged into stdout (default for assignments)") * @throws BadRequestException * @throws InvalidArgumentException */ + #[Post("version", new VInt(), "Version of the edited exercise")] + #[Post("difficulty", new VString(), "Difficulty of an exercise, should be one of 'easy', 'medium' or 'hard'")] + #[Post("localizedTexts", new VArray(), "A description of the exercise")] + #[Post("isPublic", new VBool(), "Exercise can be public or private", required: false)] + #[Post("isLocked", new VBool(), "If true, the exercise cannot be assigned", required: false)] + #[Post( + "configurationType", + new VString(), + "Identifies the way the evaluation of the exercise is configured", + required: false, + )] + #[Post( + "solutionFilesLimit", + new VInt(), + "Maximal number of files in a solution being submitted (default for assignments)", + nullable: true, + )] + #[Post( + "solutionSizeLimit", + new VInt(), + "Maximal size (bytes) of all files in a solution being submitted (default for assignments)", + nullable: true, + )] + #[Post("mergeJudgeLogs", new VBool(), "If true, judge stderr will be merged into stdout (default for assignments)")] + #[Path("id", new VString(), "identification of exercise", required: true)] public function actionUpdateDetail(string $id) { $req = $this->getRequest(); @@ -441,9 +471,9 @@ public function checkValidate($id) /** * Check if the version of the exercise is up-to-date. * @POST - * @Param(type="post", name="version", validation="numericint", description="Version of the exercise.") - * @param string $id Identifier of the exercise */ + #[Post("version", new VInt(), "Version of the exercise.")] + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionValidate($id) { $exercise = $this->exercises->findOrThrow($id); @@ -470,10 +500,10 @@ public function checkAssignments(string $id) /** * Get all non-archived assignments created from this exercise. * @GET - * @param string $id Identifier of the exercise - * @param bool $archived Include also archived groups in the result * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the exercise", required: true)] + #[Query("archived", new VBool(), "Include also archived groups in the result", required: false)] public function actionAssignments(string $id, bool $archived = false) { $exercise = $this->exercises->findOrThrow($id); @@ -491,12 +521,12 @@ function (Assignment $assignment) use ($archived) { * Create exercise with all default values. * Exercise detail can be then changed in appropriate endpoint. * @POST - * @Param(type="post", name="groupId", description="Identifier of the group to which exercise belongs to") * @throws ForbiddenRequestException * @throws NotFoundException * @throws ApiException * @throws ParseException */ + #[Post("groupId", new VString(), "Identifier of the group to which exercise belongs to")] public function actionCreate() { $user = $this->getCurrentUser(); @@ -545,12 +575,11 @@ public function checkHardwareGroups(string $id) /** * Set hardware groups which are associated with exercise. * @POST - * @param string $id identifier of exercise - * @Param(type="post", name="hwGroups", validation="array", - * description="List of hardware groups identifications to which exercise belongs to") * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Post("hwGroups", new VArray(), "List of hardware groups identifications to which exercise belongs to")] + #[Path("id", new VString(), "identifier of exercise", required: true)] public function actionHardwareGroups(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -593,8 +622,8 @@ public function checkRemove(string $id) /** * Delete an exercise * @DELETE - * @param string $id */ + #[Path("id", new VString(), required: true)] public function actionRemove(string $id) { /** @var Exercise $exercise */ @@ -607,13 +636,13 @@ public function actionRemove(string $id) /** * Fork exercise from given one into the completely new one. * @POST - * @param string $id Identifier of the exercise - * @Param(type="post", name="groupId", description="Identifier of the group to which exercise will be forked") * @throws ApiException * @throws ForbiddenRequestException * @throws NotFoundException * @throws ParseException */ + #[Post("groupId", new VString(), "Identifier of the group to which exercise will be forked")] + #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionForkFrom(string $id) { $user = $this->getCurrentUser(); @@ -652,10 +681,10 @@ public function checkAttachGroup(string $id, string $groupId) /** * Attach exercise to group with given identification. * @POST - * @param string $id Identifier of the exercise - * @param string $groupId Identifier of the group to which exercise should be attached * @throws InvalidArgumentException */ + #[Path("id", new VString(), "Identifier of the exercise", required: true)] + #[Path("groupId", new VString(), "Identifier of the group to which exercise should be attached", required: true)] public function actionAttachGroup(string $id, string $groupId) { $exercise = $this->exercises->findOrThrow($id); @@ -686,10 +715,10 @@ public function checkDetachGroup(string $id, string $groupId) /** * Detach exercise from given group. * @DELETE - * @param string $id Identifier of the exercise - * @param string $groupId Identifier of the group which should be detached from exercise * @throws InvalidArgumentException */ + #[Path("id", new VString(), "Identifier of the exercise", required: true)] + #[Path("groupId", new VString(), "Identifier of the group which should be detached from exercise", required: true)] public function actionDetachGroup(string $id, string $groupId) { $exercise = $this->exercises->findOrThrow($id); @@ -753,12 +782,15 @@ public function checkTagsUpdateGlobal(string $tag) * Update the tag globally. At the moment, the only 'update' function is 'rename'. * Other types of updates may be implemented in the future. * @POST - * @param string $tag Tag to be updated - * @Param(type="query", name="renameTo", validation="string:1..32", required=false, - * description="New name of the tag") - * @Param(type="query", name="force", validation="bool", required=false, - * description="If true, the rename will be allowed even if the new tag name exists (tags will be merged). Otherwise, name collisions will result in error.") */ + #[Query("renameTo", new VString(1, 32), "New name of the tag", required: false)] + #[Query( + "force", + new VBool(), + "If true, the rename will be allowed even if the new tag name exists (tags will be merged). Otherwise, name collisions will result in error.", + required: false, + )] + #[Path("tag", new VString(), "Tag to be updated", required: true)] public function actionTagsUpdateGlobal(string $tag, string $renameTo = null, bool $force = false) { // Check whether at least one modification action is present (so far, we have only renameTo) @@ -798,8 +830,8 @@ public function checkTagsRemoveGlobal(string $tag) /** * Remove a tag from all exercises. * @POST - * @param string $tag Tag to be removed */ + #[Path("tag", new VString(), "Tag to be removed", required: true)] public function actionTagsRemoveGlobal(string $tag) { $removeCount = $this->exerciseTags->removeTag($tag); @@ -822,15 +854,14 @@ public function checkAddTag(string $id) /** * Add tag with given name to the exercise. * @POST - * @param string $id - * @param string $name - * @Param(type="query", name="name", validation="string:1..32", - * description="Name of the newly added tag to given exercise") * @throws BadRequestException * @throws NotFoundException * @throws ForbiddenRequestException * @throws InvalidArgumentException */ + #[Query("name", new VString(1, 32), "Name of the newly added tag to given exercise")] + #[Path("id", new VString(), required: true)] + #[Path("name", new VString(), required: true)] public function actionAddTag(string $id, string $name) { if (!$this->exerciseTags->verifyTagName($name)) { @@ -860,10 +891,10 @@ public function checkRemoveTag(string $id) /** * Remove tag with given name from exercise. * @DELETE - * @param string $id - * @param string $name * @throws NotFoundException */ + #[Path("id", new VString(), required: true)] + #[Path("name", new VString(), required: true)] public function actionRemoveTag(string $id, string $name) { $exercise = $this->exercises->findOrThrow($id); @@ -888,11 +919,10 @@ public function checkSetArchived(string $id) /** * (Un)mark the exercise as archived. Nothing happens if the exercise is already in the requested state. * @POST - * @param string $id identifier of the exercise - * @Param(type="post", name="archived", required=true, validation=boolean, - * description="Whether the exercise should be marked or unmarked") * @throws NotFoundException */ + #[Post("archived", new VBool(), "Whether the exercise should be marked or unmarked", required: true)] + #[Path("id", new VString(), "identifier of the exercise", required: true)] public function actionSetArchived(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -918,12 +948,11 @@ public function checkSetAuthor(string $id) /** * Change the author of the exercise. This is a very special operation reserved for powerful users. * @POST - * @param string $id identifier of the exercise - * @Param(type="post", name="author", required=true, validation="string:36", - * description="Id of the new author of the exercise.") * @throws NotFoundException * @throws ForbiddenRequestException */ + #[Post("author", new VUuid(), "Id of the new author of the exercise.", required: true)] + #[Path("id", new VString(), "identifier of the exercise", required: true)] public function actionSetAuthor(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -957,10 +986,10 @@ public function checkSetAdmins(string $id) * Change who the admins of an exercise are (all users on the list are added, * prior admins not on the list are removed). * @POST - * @param string $id identifier of the exercise - * @Param(type="post", name="admins", required=true, validation=array, description="List of user IDs.") * @throws NotFoundException */ + #[Post("admins", new VArray(), "List of user IDs.", required: true)] + #[Path("id", new VString(), "identifier of the exercise", required: true)] public function actionSetAdmins(string $id) { $exercise = $this->exercises->findOrThrow($id); @@ -1017,9 +1046,9 @@ public function checkSendNotification(string $id) * or the exercise is modified significantly. * The response is number of emails sent (number of notified users). * @POST - * @param string $id identifier of the exercise - * @Param(type="post", name="message", validation=string, description="Message sent to notified users.") */ + #[Post("message", new VString(), "Message sent to notified users.")] + #[Path("id", new VString(), "identifier of the exercise", required: true)] public function actionSendNotification(string $id) { $exercise = $this->exercises->findOrThrow($id); diff --git a/app/V1Module/presenters/ExtensionsPresenter.php b/app/V1Module/presenters/ExtensionsPresenter.php index 72ccd3c1f..721318a7c 100644 --- a/app/V1Module/presenters/ExtensionsPresenter.php +++ b/app/V1Module/presenters/ExtensionsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\BadRequestException; use App\Model\Repository\Instances; @@ -59,9 +71,11 @@ public function checkUrl(string $extId, string $instanceId) /** * Return URL refering to the extension with properly injected temporary JWT token. * @GET - * @Param(type="query", name="locale", required=false, validation="string:2") - * @Param(type="query", name="return", required=false, validation="string") */ + #[Query("locale", new VString(2, 2), required: false)] + #[Query("return", new VString(), required: false)] + #[Path("extId", new VString(), required: true)] + #[Path("instanceId", new VString(), required: true)] public function actionUrl(string $extId, string $instanceId, ?string $locale, ?string $return) { $user = $this->getCurrentUser(); @@ -117,6 +131,7 @@ public function checkToken(string $extId) * (from a temp token passed via URL). It also returns details about authenticated user. * @POST */ + #[Path("extId", new VString(), required: true)] public function actionToken(string $extId) { $user = $this->getCurrentUser(); diff --git a/app/V1Module/presenters/ForgottenPasswordPresenter.php b/app/V1Module/presenters/ForgottenPasswordPresenter.php index 0a7462670..a85acc6a2 100644 --- a/app/V1Module/presenters/ForgottenPasswordPresenter.php +++ b/app/V1Module/presenters/ForgottenPasswordPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\NotFoundException; use App\Helpers\ForgottenPasswordHelper; @@ -47,10 +59,9 @@ class ForgottenPasswordPresenter extends BasePresenter /** * Request a password reset (user will receive an e-mail that prompts them to reset their password) * @POST - * @Param(type="post", name="username", validation="string:2..", - * description="An identifier of the user whose password should be reset") * @throws NotFoundException */ + #[Post("username", new VString(2), "An identifier of the user whose password should be reset")] public function actionDefault() { $req = $this->getHttpRequest(); @@ -66,10 +77,10 @@ public function actionDefault() /** * Change the user's password * @POST - * @Param(type="post", name="password", validation="string:2..", description="The new password") * @LoggedIn * @throws ForbiddenRequestException */ + #[Post("password", new VString(2), "The new password")] public function actionChange() { if (!$this->isInScope(TokenScope::CHANGE_PASSWORD)) { @@ -98,8 +109,8 @@ public function actionChange() /** * Check if a password is strong enough * @POST - * @Param(type="post", name="password", description="The password to be checked") */ + #[Post("password", new VString(), "The password to be checked")] public function actionValidatePasswordStrength() { $password = $this->getRequest()->getPost("password"); diff --git a/app/V1Module/presenters/GroupExternalAttributesPresenter.php b/app/V1Module/presenters/GroupExternalAttributesPresenter.php index f2684fa59..5fac3b588 100644 --- a/app/V1Module/presenters/GroupExternalAttributesPresenter.php +++ b/app/V1Module/presenters/GroupExternalAttributesPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\BadRequestException; use App\Model\Repository\GroupExternalAttributes; @@ -52,8 +64,6 @@ public function checkDefault() /** * Return all attributes that correspond to given filtering parameters. * @GET - * @Param(type="query", name="filter", required=true, validation="string", - * description="JSON-encoded filter query in DNF as [clause OR clause...]") * * The filter is encocded as array of objects (logically represented as disjunction of clauses) * -- i.e., [clause1 OR clause2 ...]. Each clause is an object with the following keys: @@ -64,6 +74,7 @@ public function checkDefault() * * The endpoint will return a list of matching attributes and all related group entities. */ + #[Query("filter", new VString(), "JSON-encoded filter query in DNF as [clause OR clause...]", required: true)] public function actionDefault(?string $filter) { $filterStruct = json_decode($filter ?? '', true); @@ -99,14 +110,12 @@ public function checkAdd() /** * Create an external attribute for given group. - * @Param(type="post", name="service", required=true, validation="string:1..32", - * description="Identifier of the external service creating the attribute") - * @Param(type="post", name="key", required=true, validation="string:1..32", - * description="Key of the attribute (must be valid identifier)") - * @Param(type="post", name="value", required=true, validation="string:0..255", - * description="Value of the attribute (arbitrary string)") * @POST */ + #[Post("service", new VString(1, 32), "Identifier of the external service creating the attribute", required: true)] + #[Post("key", new VString(1, 32), "Key of the attribute (must be valid identifier)", required: true)] + #[Post("value", new VString(0, 255), "Value of the attribute (arbitrary string)", required: true)] + #[Path("groupId", new VString(), required: true)] public function actionAdd(string $groupId) { $group = $this->groups->findOrThrow($groupId); @@ -132,6 +141,7 @@ public function checkRemove() * Remove selected attribute * @DELETE */ + #[Path("id", new VString(), required: true)] public function actionRemove(string $id) { $attribute = $this->groupExternalAttributes->findOrThrow($id); diff --git a/app/V1Module/presenters/GroupInvitationsPresenter.php b/app/V1Module/presenters/GroupInvitationsPresenter.php index f8ec13fbe..1299667f0 100644 --- a/app/V1Module/presenters/GroupInvitationsPresenter.php +++ b/app/V1Module/presenters/GroupInvitationsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Model\Repository\GroupInvitations; use App\Model\Repository\Groups; @@ -51,6 +63,7 @@ public function checkDefault($id) * Return invitation details including all relevant group entities (so a name can be constructed). * @GET */ + #[Path("id", new VString(), required: true)] public function actionDefault($id) { $invitation = $this->groupInvitations->findOrThrow($id); @@ -72,9 +85,10 @@ public function checkUpdate($id) /** * Edit the invitation. * @POST - * @Param(name="expireAt", type="post", validation="timestamp|null", description="When the invitation expires.") - * @Param(name="note", type="post", description="Note for the students who wish to use the invitation link.") */ + #[Post("expireAt", new VTimestamp(), "When the invitation expires.", nullable: true)] + #[Post("note", new VString(), "Note for the students who wish to use the invitation link.")] + #[Path("id", new VString(), required: true)] public function actionUpdate($id) { $req = $this->getRequest(); @@ -97,6 +111,7 @@ public function checkRemove($id) /** * @DELETE */ + #[Path("id", new VString(), required: true)] public function actionRemove($id) { $invitation = $this->groupInvitations->findOrThrow($id); @@ -121,6 +136,7 @@ public function checkAccept($id) * Allow the current user to join the corresponding group using the invitation. * @POST */ + #[Path("id", new VString(), required: true)] public function actionAccept($id) { $invitation = $this->groupInvitations->findOrThrow($id); @@ -145,6 +161,7 @@ public function checkList($groupId) * List all invitations of a group. * @GET */ + #[Path("groupId", new VString(), required: true)] public function actionList($groupId) { $group = $this->groups->findOrThrow($groupId); @@ -162,9 +179,10 @@ public function checkCreate($groupId) /** * Create a new invitation for given group. * @POST - * @Param(name="expireAt", type="post", validation="timestamp|null", description="When the invitation expires.") - * @Param(name="note", type="post", description="Note for the students who wish to use the invitation link.") */ + #[Post("expireAt", new VTimestamp(), "When the invitation expires.", nullable: true)] + #[Post("note", new VString(), "Note for the students who wish to use the invitation link.")] + #[Path("groupId", new VString(), required: true)] public function actionCreate($groupId) { $req = $this->getRequest(); diff --git a/app/V1Module/presenters/GroupsPresenter.php b/app/V1Module/presenters/GroupsPresenter.php index accca0407..818b575a7 100644 --- a/app/V1Module/presenters/GroupsPresenter.php +++ b/app/V1Module/presenters/GroupsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\InvalidArgumentException; use App\Exceptions\NotFoundException; use App\Exceptions\BadRequestException; @@ -170,14 +182,27 @@ class GroupsPresenter extends BasePresenter /** * Get a list of all non-archived groups a user can see. The return set is filtered by parameters. * @GET - * @param string|null $instanceId Only groups of this instance are returned. - * @param bool $ancestors If true, returns an ancestral closure of the initial result set. - * Included ancestral groups do not respect other filters (archived, search, ...). - * @param string|null $search Search string. Only groups containing this string as - * a substring of their names are returned. - * @param bool $archived Include also archived groups in the result. - * @param bool $onlyArchived Automatically implies $archived flag and returns only archived groups. */ + #[Query("instanceId", new VString(), "Only groups of this instance are returned.", required: false)] + #[Query( + "ancestors", + new VBool(), + "If true, returns an ancestral closure of the initial result set. Included ancestral groups do not respect other filters (archived, search, ...).", + required: false, + )] + #[Query( + "search", + new VString(), + "Search string. Only groups containing this string as a substring of their names are returned.", + required: false, + )] + #[Query("archived", new VBool(), "Include also archived groups in the result.", required: false)] + #[Query( + "onlyArchived", + new VBool(), + "Automatically implies \$archived flag and returns only archived groups.", + required: false, + )] public function actionDefault( string $instanceId = null, bool $ancestors = false, @@ -226,33 +251,36 @@ private function setGroupPoints(Request $req, Group $group): void /** * Create a new group * @POST - * @Param(type="post", name="instanceId", validation="string:36", - * description="An identifier of the instance where the group should be created") - * @Param(type="post", name="externalId", required=false, - * description="An informative, human readable identifier of the group") - * @Param(type="post", name="parentGroupId", validation="string:36", required=false, - * description="Identifier of the parent group (if none is given, a top-level group is created)") - * @Param(type="post", name="publicStats", validation="bool", required=false, - * description="Should students be able to see each other's results?") - * @Param(type="post", name="detaining", validation="bool", required=false, - * description="Are students prevented from leaving the group on their own?") - * @Param(type="post", name="isPublic", validation="bool", required=false, - * description="Should the group be visible to all student?") - * @Param(type="post", name="isOrganizational", validation="bool", required=false, - * description="Whether the group is organizational (no assignments nor students).") - * @Param(type="post", name="isExam", validation="bool", required=false, - * description="Whether the group is an exam group.") - * @Param(type="post", name="localizedTexts", validation="array", required=false, - * description="Localized names and descriptions") - * @Param(type="post", name="threshold", validation="numericint", required=false, - * description="A minimum percentage of points needed to pass the course") - * @Param(type="post", name="pointsLimit", validation="numericint", required=false, - * description="A minimum of (absolute) points needed to pass the course") - * @Param(type="post", name="noAdmin", validation="bool", required=false, - * description="If true, no admin is assigned to group (current user is assigned as admin by default.") * @throws ForbiddenRequestException * @throws InvalidArgumentException */ + #[Post("instanceId", new VUuid(), "An identifier of the instance where the group should be created")] + #[Post("externalId", new VString(), "An informative, human readable identifier of the group", required: false)] + #[Post( + "parentGroupId", + new VUuid(), + "Identifier of the parent group (if none is given, a top-level group is created)", + required: false, + )] + #[Post("publicStats", new VBool(), "Should students be able to see each other's results?", required: false)] + #[Post("detaining", new VBool(), "Are students prevented from leaving the group on their own?", required: false)] + #[Post("isPublic", new VBool(), "Should the group be visible to all student?", required: false)] + #[Post( + "isOrganizational", + new VBool(), + "Whether the group is organizational (no assignments nor students).", + required: false, + )] + #[Post("isExam", new VBool(), "Whether the group is an exam group.", required: false)] + #[Post("localizedTexts", new VArray(), "Localized names and descriptions", required: false)] + #[Post("threshold", new VInt(), "A minimum percentage of points needed to pass the course", required: false)] + #[Post("pointsLimit", new VInt(), "A minimum of (absolute) points needed to pass the course", required: false)] + #[Post( + "noAdmin", + new VBool(), + "If true, no admin is assigned to group (current user is assigned as admin by default.", + required: false, + )] public function actionAddGroup() { $req = $this->getRequest(); @@ -308,12 +336,12 @@ public function actionAddGroup() /** * Validate group creation data * @POST - * @Param(name="name", type="post", description="Name of the group") - * @Param(name="locale", type="post", description="The locale of the name") - * @Param(name="instanceId", type="post", description="Identifier of the instance where the group belongs") - * @Param(name="parentGroupId", type="post", required=false, description="Identifier of the parent group") * @throws ForbiddenRequestException */ + #[Post("name", new VString(), "Name of the group")] + #[Post("locale", new VString(), "The locale of the name")] + #[Post("instanceId", new VString(), "Identifier of the instance where the group belongs")] + #[Post("parentGroupId", new VString(), "Identifier of the parent group", required: false)] public function actionValidateAddGroupData() { $req = $this->getRequest(); @@ -345,22 +373,16 @@ public function checkUpdateGroup(string $id) /** * Update group info * @POST - * @Param(type="post", name="externalId", required=false, - * description="An informative, human readable indentifier of the group") - * @Param(type="post", name="publicStats", validation="bool", - * description="Should students be able to see each other's results?") - * @Param(type="post", name="detaining", validation="bool", - * required=false, description="Are students prevented from leaving the group on their own?") - * @Param(type="post", name="isPublic", validation="bool", - * description="Should the group be visible to all student?") - * @Param(type="post", name="threshold", validation="numericint", required=false, - * description="A minimum percentage of points needed to pass the course") - * @Param(type="post", name="pointsLimit", validation="numericint", required=false, - * description="A minimum of (absolute) points needed to pass the course") - * @Param(type="post", name="localizedTexts", validation="array", description="Localized names and descriptions") - * @param string $id An identifier of the updated group * @throws InvalidArgumentException */ + #[Post("externalId", new VString(), "An informative, human readable indentifier of the group", required: false)] + #[Post("publicStats", new VBool(), "Should students be able to see each other's results?")] + #[Post("detaining", new VBool(), "Are students prevented from leaving the group on their own?", required: false)] + #[Post("isPublic", new VBool(), "Should the group be visible to all student?")] + #[Post("threshold", new VInt(), "A minimum percentage of points needed to pass the course", required: false)] + #[Post("pointsLimit", new VInt(), "A minimum of (absolute) points needed to pass the course", required: false)] + #[Post("localizedTexts", new VArray(), "Localized names and descriptions")] + #[Path("id", new VString(), "An identifier of the updated group", required: true)] public function actionUpdateGroup(string $id) { $req = $this->getRequest(); @@ -397,11 +419,11 @@ public function checkSetOrganizational(string $id) /** * Set the 'isOrganizational' flag for a group * @POST - * @Param(type="post", name="value", validation="bool", required=true, description="The value of the flag") - * @param string $id An identifier of the updated group * @throws BadRequestException * @throws NotFoundException */ + #[Post("value", new VBool(), "The value of the flag", required: true)] + #[Path("id", new VString(), "An identifier of the updated group", required: true)] public function actionSetOrganizational(string $id) { $group = $this->groups->findOrThrow($id); @@ -434,10 +456,10 @@ public function checkSetArchived(string $id) /** * Set the 'isArchived' flag for a group * @POST - * @Param(type="post", name="value", validation="bool", required=true, description="The value of the flag") - * @param string $id An identifier of the updated group * @throws NotFoundException */ + #[Post("value", new VBool(), "The value of the flag", required: true)] + #[Path("id", new VString(), "An identifier of the updated group", required: true)] public function actionSetArchived(string $id) { $group = $this->groups->findOrThrow($id); @@ -515,11 +537,11 @@ public function checkSetExam(string $id) * Change the group "exam" indicator. If denotes that the group should be listed in exam groups instead of * regular groups and the assignments should have "isExam" flag set by default. * @POST - * @Param(type="post", name="value", validation="bool", required=true, description="The value of the flag") - * @param string $id An identifier of the updated group * @throws BadRequestException * @throws NotFoundException */ + #[Post("value", new VBool(), "The value of the flag", required: true)] + #[Path("id", new VString(), "An identifier of the updated group", required: true)] public function actionSetExam(string $id) { $group = $this->groups->findOrThrow($id); @@ -543,15 +565,23 @@ public function checkSetExamPeriod(string $id) * This endpoint is also used to update already planned exam period, but only dates in the future * can be editted (e.g., once an exam begins, the beginning may no longer be updated). * @POST - * @Param(type="post", name="begin", validation="timestamp|null", required=false, - * description="When the exam begins (unix ts in the future, optional if update is performed).") - * @Param(type="post", name="end", validation="timestamp", required=true, - * description="When the exam ends (unix ts in the future, no more than a day after 'begin').") - * @Param(type="post", name="strict", validation="bool", required=false, - * description="Whether locked users are prevented from accessing other groups.") - * @param string $id An identifier of the updated group * @throws NotFoundException */ + #[Post( + "begin", + new VTimestamp(), + "When the exam begins (unix ts in the future, optional if update is performed).", + required: false, + nullable: true, + )] + #[Post( + "end", + new VTimestamp(), + "When the exam ends (unix ts in the future, no more than a day after 'begin').", + required: true, + )] + #[Post("strict", new VBool(), "Whether locked users are prevented from accessing other groups.", required: false)] + #[Path("id", new VString(), "An identifier of the updated group", required: true)] public function actionSetExamPeriod(string $id) { $group = $this->groups->findOrThrow($id); @@ -662,9 +692,9 @@ public function checkRemoveExamPeriod(string $id) /** * Change the group back to regular group (remove information about an exam). * @DELETE - * @param string $id An identifier of the updated group * @throws NotFoundException */ + #[Path("id", new VString(), "An identifier of the updated group", required: true)] public function actionRemoveExamPeriod(string $id) { $group = $this->groups->findOrThrow($id); @@ -712,9 +742,9 @@ public function checkGetExamLocks(string $id, string $examId) /** * Retrieve a list of locks for given exam * @GET - * @param string $id An identifier of the related group - * @param string $examId An identifier of the exam */ + #[Path("id", new VString(), "An identifier of the related group", required: true)] + #[Path("examId", new VString(), "An identifier of the exam", required: true)] public function actionGetExamLocks(string $id, string $examId) { $group = $this->groups->findOrThrow($id); @@ -726,11 +756,11 @@ public function actionGetExamLocks(string $id, string $examId) /** * Relocate the group under a different parent. * @POST - * @param string $id An identifier of the relocated group - * @param string $newParentId An identifier of the new parent group * @throws NotFoundException * @throws BadRequestException */ + #[Path("id", new VString(), "An identifier of the relocated group", required: true)] + #[Path("newParentId", new VString(), "An identifier of the new parent group", required: true)] public function actionRelocate(string $id, string $newParentId) { $group = $this->groups->findOrThrow($id); @@ -786,8 +816,8 @@ public function checkRemoveGroup(string $id) /** * Delete a group * @DELETE - * @param string $id Identifier of the group */ + #[Path("id", new VString(), "Identifier of the group", required: true)] public function actionRemoveGroup(string $id) { $group = $this->groups->findOrThrow($id); @@ -809,8 +839,8 @@ public function checkDetail(string $id) /** * Get details of a group * @GET - * @param string $id Identifier of the group */ + #[Path("id", new VString(), "Identifier of the group", required: true)] public function actionDetail(string $id) { $group = $this->groups->findOrThrow($id); @@ -830,9 +860,9 @@ public function checkSubgroups(string $id) /** * Get a list of subgroups of a group * @GET - * @param string $id Identifier of the group * @DEPRECTATED Subgroup list is part of group view. */ + #[Path("id", new VString(), "Identifier of the group", required: true)] public function actionSubgroups(string $id) { /** @var Group $group */ @@ -861,9 +891,9 @@ public function checkMembers(string $id) /** * Get a list of members of a group * @GET - * @param string $id Identifier of the group * @DEPRECATED Members are listed in group view. */ + #[Path("id", new VString(), "Identifier of the group", required: true)] public function actionMembers(string $id) { $group = $this->groups->findOrThrow($id); @@ -895,11 +925,10 @@ public function checkAddMember(string $id, string $userId) /** * Add/update a membership (other than student) for given user * @POST - * @Param(type="post", name="type", validation="string:1..", required=true, - * description="Identifier of membership type (admin, supervisor, ...)") - * @param string $id Identifier of the group - * @param string $userId Identifier of the supervisor */ + #[Post("type", new VString(1), "Identifier of membership type (admin, supervisor, ...)", required: true)] + #[Path("id", new VString(), "Identifier of the group", required: true)] + #[Path("userId", new VString(), "Identifier of the supervisor", required: true)] public function actionAddMember(string $id, string $userId) { $user = $this->users->findOrThrow($userId); @@ -942,9 +971,9 @@ public function checkRemoveMember(string $id, string $userId) /** * Remove a member (other than student) from a group * @DELETE - * @param string $id Identifier of the group - * @param string $userId Identifier of the supervisor */ + #[Path("id", new VString(), "Identifier of the group", required: true)] + #[Path("userId", new VString(), "Identifier of the supervisor", required: true)] public function actionRemoveMember(string $id, string $userId) { $user = $this->users->findOrThrow($userId); @@ -980,8 +1009,8 @@ public function checkAssignments(string $id) /** * Get all exercise assignments for a group * @GET - * @param string $id Identifier of the group */ + #[Path("id", new VString(), "Identifier of the group", required: true)] public function actionAssignments(string $id) { /** @var Group $group */ @@ -1014,8 +1043,8 @@ public function checkShadowAssignments(string $id) /** * Get all shadow assignments for a group * @GET - * @param string $id Identifier of the group */ + #[Path("id", new VString(), "Identifier of the group", required: true)] public function actionShadowAssignments(string $id) { /** @var Group $group */ @@ -1052,9 +1081,9 @@ public function checkStats(string $id) * Get statistics of a group. If the user does not have the rights to view all of these, try to at least * return their statistics. * @GET - * @param string $id Identifier of the group * @throws ForbiddenRequestException */ + #[Path("id", new VString(), "Identifier of the group", required: true)] public function actionStats(string $id) { $group = $this->groups->findOrThrow($id); @@ -1081,10 +1110,10 @@ public function checkStudentsStats(string $id, string $userId) /** * Get statistics of a single student in a group * @GET - * @param string $id Identifier of the group - * @param string $userId Identifier of the student * @throws BadRequestException */ + #[Path("id", new VString(), "Identifier of the group", required: true)] + #[Path("userId", new VString(), "Identifier of the student", required: true)] public function actionStudentsStats(string $id, string $userId) { $user = $this->users->findOrThrow($userId); @@ -1111,10 +1140,10 @@ public function checkStudentsSolutions(string $id, string $userId) /** * Get all solutions of a single student from all assignments in a group * @GET - * @param string $id Identifier of the group - * @param string $userId Identifier of the student * @throws BadRequestException */ + #[Path("id", new VString(), "Identifier of the group", required: true)] + #[Path("userId", new VString(), "Identifier of the student", required: true)] public function actionStudentsSolutions(string $id, string $userId) { $user = $this->users->findOrThrow($userId); @@ -1159,9 +1188,9 @@ public function checkAddStudent(string $id, string $userId) /** * Add a student to a group * @POST - * @param string $id Identifier of the group - * @param string $userId Identifier of the student */ + #[Path("id", new VString(), "Identifier of the group", required: true)] + #[Path("userId", new VString(), "Identifier of the student", required: true)] public function actionAddStudent(string $id, string $userId) { $user = $this->users->findOrThrow($userId); @@ -1190,9 +1219,9 @@ public function checkRemoveStudent(string $id, string $userId) /** * Remove a student from a group * @DELETE - * @param string $id Identifier of the group - * @param string $userId Identifier of the student */ + #[Path("id", new VString(), "Identifier of the group", required: true)] + #[Path("userId", new VString(), "Identifier of the student", required: true)] public function actionRemoveStudent(string $id, string $userId) { $user = $this->users->findOrThrow($userId); @@ -1222,9 +1251,9 @@ public function checkLockStudent(string $id, string $userId) /** * Lock student in a group and with an IP from which the request was made. * @POST - * @param string $id Identifier of the group - * @param string $userId Identifier of the student */ + #[Path("id", new VString(), "Identifier of the group", required: true)] + #[Path("userId", new VString(), "Identifier of the student", required: true)] public function actionLockStudent(string $id, string $userId) { $user = $this->users->findOrThrow($userId); @@ -1262,9 +1291,9 @@ public function checkUnlockStudent(string $id, string $userId) /** * Unlock a student currently locked in a group. * @DELETE - * @param string $id Identifier of the group - * @param string $userId Identifier of the student */ + #[Path("id", new VString(), "Identifier of the group", required: true)] + #[Path("userId", new VString(), "Identifier of the student", required: true)] public function actionUnlockStudent(string $id, string $userId) { $user = $this->users->findOrThrow($userId); diff --git a/app/V1Module/presenters/HardwareGroupsPresenter.php b/app/V1Module/presenters/HardwareGroupsPresenter.php index 305f11804..10872c00e 100644 --- a/app/V1Module/presenters/HardwareGroupsPresenter.php +++ b/app/V1Module/presenters/HardwareGroupsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Model\Repository\HardwareGroups; use App\Security\ACL\IHardwareGroupPermissions; diff --git a/app/V1Module/presenters/InstancesPresenter.php b/app/V1Module/presenters/InstancesPresenter.php index 151c51587..84784697e 100644 --- a/app/V1Module/presenters/InstancesPresenter.php +++ b/app/V1Module/presenters/InstancesPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\NotFoundException; use App\Model\Entity\LocalizedGroup; @@ -106,12 +118,11 @@ public function checkCreateInstance() /** * Create a new instance * @POST - * @Param(type="post", name="name", validation="string:2..", description="Name of the instance") - * @Param(type="post", name="description", required=false, description="Description of the instance") - * @Param(type="post", name="isOpen", validation="bool", - * description="Should the instance be open for registration?") * @throws ForbiddenRequestException */ + #[Post("name", new VString(2), "Name of the instance")] + #[Post("description", new VString(), "Description of the instance", required: false)] + #[Post("isOpen", new VBool(), "Should the instance be open for registration?")] public function actionCreateInstance() { $req = $this->getRequest(); @@ -144,10 +155,9 @@ public function checkUpdateInstance(string $id) /** * Update an instance * @POST - * @Param(type="post", name="isOpen", validation="bool", required=false, - * description="Should the instance be open for registration?") - * @param string $id An identifier of the updated instance */ + #[Post("isOpen", new VBool(), "Should the instance be open for registration?", required: false)] + #[Path("id", new VString(), "An identifier of the updated instance", required: true)] public function actionUpdateInstance(string $id) { $instance = $this->instances->findOrThrow($id); @@ -174,8 +184,8 @@ public function checkDeleteInstance(string $id) /** * Delete an instance * @DELETE - * @param string $id An identifier of the instance to be deleted */ + #[Path("id", new VString(), "An identifier of the instance to be deleted", required: true)] public function actionDeleteInstance(string $id) { $instance = $this->instances->findOrThrow($id); @@ -204,8 +214,8 @@ public function checkDetail(string $id) /** * Get details of an instance * @GET - * @param string $id An identifier of the instance */ + #[Path("id", new VString(), "An identifier of the instance", required: true)] public function actionDetail(string $id) { $instance = $this->instances->findOrThrow($id); @@ -223,8 +233,8 @@ public function checkLicences(string $id) /** * Get a list of licenses associated with an instance * @GET - * @param string $id An identifier of the instance */ + #[Path("id", new VString(), "An identifier of the instance", required: true)] public function actionLicences(string $id) { $instance = $this->instances->findOrThrow($id); @@ -242,10 +252,10 @@ public function checkCreateLicence(string $id) /** * Create a new license for an instance * @POST - * @Param(type="post", name="note", validation="string:2..", description="A note for users or administrators") - * @Param(type="post", name="validUntil", validation="timestamp", description="Expiration date of the license") - * @param string $id An identifier of the instance */ + #[Post("note", new VString(2), "A note for users or administrators")] + #[Post("validUntil", new VTimestamp(), "Expiration date of the license")] + #[Path("id", new VString(), "An identifier of the instance", required: true)] public function actionCreateLicence(string $id) { $instance = $this->instances->findOrThrow($id); @@ -269,15 +279,12 @@ public function checkUpdateLicence(string $licenceId) /** * Update an existing license for an instance * @POST - * @Param(type="post", name="note", validation="string:2..255", required=false, - * description="A note for users or administrators") - * @Param(type="post", name="validUntil", validation="string", required=false, - * description="Expiration date of the license") - * @Param(type="post", name="isValid", validation="bool", required=false, - * description="Administrator switch to toggle licence validity") - * @param string $licenceId Identifier of the licence * @throws NotFoundException */ + #[Post("note", new VString(2, 255), "A note for users or administrators", required: false)] + #[Post("validUntil", new VString(), "Expiration date of the license", required: false)] + #[Post("isValid", new VBool(), "Administrator switch to toggle licence validity", required: false)] + #[Path("licenceId", new VString(), "Identifier of the licence", required: true)] public function actionUpdateLicence(string $licenceId) { $licence = $this->licences->findOrThrow($licenceId); @@ -309,9 +316,9 @@ public function checkDeleteLicence(string $licenceId) /** * Remove existing license for an instance * @DELETE - * @param string $licenceId Identifier of the licence * @throws NotFoundException */ + #[Path("licenceId", new VString(), "Identifier of the licence", required: true)] public function actionDeleteLicence(string $licenceId) { $licence = $this->licences->findOrThrow($licenceId); diff --git a/app/V1Module/presenters/LoginPresenter.php b/app/V1Module/presenters/LoginPresenter.php index b243102bf..421167377 100644 --- a/app/V1Module/presenters/LoginPresenter.php +++ b/app/V1Module/presenters/LoginPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\FrontendErrorMappings; @@ -107,13 +119,13 @@ private function sendAccessTokenResponse(User $user) /** * Log in using user credentials * @POST - * @Param(type="post", name="username", validation="email:1..", description="User's E-mail") - * @Param(type="post", name="password", validation="string:1..", description="Password") * @throws AuthenticationException * @throws ForbiddenRequestException * @throws InvalidAccessTokenException * @throws WrongCredentialsException */ + #[Post("username", new VEmail(), "User's E-mail")] + #[Post("password", new VString(1), "Password")] public function actionDefault() { $req = $this->getRequest(); @@ -134,14 +146,14 @@ public function actionDefault() /** * Log in using an external authentication service * @POST - * @Param(type="post", name="token", validation="string:1..", description="JWT external authentication token") - * @param string $authenticatorName Identifier of the external authenticator * @throws AuthenticationException * @throws ForbiddenRequestException * @throws InvalidAccessTokenException * @throws WrongCredentialsException * @throws BadRequestException */ + #[Post("token", new VString(1), "JWT external authentication token")] + #[Path("authenticatorName", new VString(), "Identifier of the external authenticator", required: true)] public function actionExternal($authenticatorName) { $req = $this->getRequest(); @@ -168,11 +180,11 @@ public function checkTakeOver($userId) * Takeover user account with specified user identification. * @POST * @LoggedIn - * @param string $userId * @throws AuthenticationException * @throws ForbiddenRequestException * @throws InvalidAccessTokenException */ + #[Path("userId", new VString(), required: true)] public function actionTakeOver($userId) { $user = $this->users->findOrThrow($userId); @@ -235,15 +247,13 @@ public function checkIssueRestrictedToken() * Issue a new access token with a restricted set of scopes * @POST * @LoggedIn - * @Param(type="post", name="effectiveRole", required=false, validation="string", - * description="Effective user role contained within issued token") - * @Param(type="post", name="scopes", validation="list", description="A list of requested scopes") - * @Param(type="post", required=false, name="expiration", validation="numericint", - * description="How long should the token be valid (in seconds)") * @throws BadRequestException * @throws ForbiddenRequestException * @throws InvalidArgumentException */ + #[Post("effectiveRole", new VString(), "Effective user role contained within issued token", required: false)] + #[Post("scopes", new VArray(), "A list of requested scopes")] + #[Post("expiration", new VInt(), "How long should the token be valid (in seconds)", required: false)] public function actionIssueRestrictedToken() { $request = $this->getRequest(); diff --git a/app/V1Module/presenters/NotificationsPresenter.php b/app/V1Module/presenters/NotificationsPresenter.php index 5be144f33..2f088d7ba 100644 --- a/app/V1Module/presenters/NotificationsPresenter.php +++ b/app/V1Module/presenters/NotificationsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\InvalidArgumentException; use App\Exceptions\NotFoundException; @@ -54,8 +66,8 @@ public function checkDefault() * returns only the ones from given groups (and their ancestors) and * global ones (without group). * @GET - * @param array $groupsIds identifications of groups */ + #[Query("groupsIds", new VArray(), "identifications of groups", required: false)] public function actionDefault(array $groupsIds) { $ancestralGroupsIds = $this->groups->groupsIdsAncestralClosure($groupsIds); @@ -103,21 +115,17 @@ public function checkCreate() /** * Create notification with given attributes - * @Param(type="post", name="groupsIds", validation="array", - * description="Identification of groups") - * @Param(type="post", name="visibleFrom", validation="timestamp", - * description="Date from which is notification visible") - * @Param(type="post", name="visibleTo", validation="timestamp", - * description="Date to which is notification visible") - * @Param(type="post", name="role", validation="string:1..", - * description="Users with this role and its children can see notification") - * @Param(type="post", name="type", validation="string", description="Type of the notification (custom)") - * @Param(type="post", name="localizedTexts", validation="array", description="Text of notification") * @POST * @throws NotFoundException * @throws ForbiddenRequestException * @throws InvalidArgumentException */ + #[Post("groupsIds", new VArray(), "Identification of groups")] + #[Post("visibleFrom", new VTimestamp(), "Date from which is notification visible")] + #[Post("visibleTo", new VTimestamp(), "Date to which is notification visible")] + #[Post("role", new VString(1), "Users with this role and its children can see notification")] + #[Post("type", new VString(), "Type of the notification (custom)")] + #[Post("localizedTexts", new VArray(), "Text of notification")] public function actionCreate() { $notification = new Notification($this->getCurrentUser()); @@ -220,20 +228,17 @@ public function checkUpdate(string $id) /** * Update notification * @POST - * @param string $id - * @Param(type="post", name="groupsIds", validation="array", description="Identification of groups") - * @Param(type="post", name="visibleFrom", validation="timestamp", - * description="Date from which is notification visible") - * @Param(type="post", name="visibleTo", validation="timestamp", - * description="Date to which is notification visible") - * @Param(type="post", name="role", validation="string:1..", - * description="Users with this role and its children can see notification") - * @Param(type="post", name="type", validation="string", description="Type of the notification (custom)") - * @Param(type="post", name="localizedTexts", validation="array", description="Text of notification") * @throws NotFoundException * @throws ForbiddenRequestException * @throws InvalidArgumentException */ + #[Post("groupsIds", new VArray(), "Identification of groups")] + #[Post("visibleFrom", new VTimestamp(), "Date from which is notification visible")] + #[Post("visibleTo", new VTimestamp(), "Date to which is notification visible")] + #[Post("role", new VString(1), "Users with this role and its children can see notification")] + #[Post("type", new VString(), "Type of the notification (custom)")] + #[Post("localizedTexts", new VArray(), "Text of notification")] + #[Path("id", new VString(), required: true)] public function actionUpdate(string $id) { $notification = $this->notifications->findOrThrow($id); @@ -253,9 +258,9 @@ public function checkRemove(string $id) /** * Delete a notification * @DELETE - * @param string $id * @throws NotFoundException */ + #[Path("id", new VString(), required: true)] public function actionRemove(string $id) { $notification = $this->notifications->findOrThrow($id); diff --git a/app/V1Module/presenters/PipelinesPresenter.php b/app/V1Module/presenters/PipelinesPresenter.php index 91f52acc3..4f1399905 100644 --- a/app/V1Module/presenters/PipelinesPresenter.php +++ b/app/V1Module/presenters/PipelinesPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Exceptions\ExerciseConfigException; use App\Exceptions\ForbiddenRequestException; @@ -141,12 +153,22 @@ public function checkDefault(string $search = null) * Get a list of pipelines with an optional filter, ordering, and pagination pruning. * The result conforms to pagination protocol. * @GET - * @param int $offset Index of the first result. - * @param int|null $limit Maximal number of results returned. - * @param string|null $orderBy Name of the column (column concept). The '!' prefix indicate descending order. - * @param array|null $filters Named filters that prune the result. - * @param string|null $locale Currently set locale (used to augment order by clause if necessary), */ + #[Query("offset", new VInt(), "Index of the first result.", required: false)] + #[Query("limit", new VInt(), "Maximal number of results returned.", required: false)] + #[Query( + "orderBy", + new VString(), + "Name of the column (column concept). The '!' prefix indicate descending order.", + required: false, + )] + #[Query("filters", new VArray(), "Named filters that prune the result.", required: false)] + #[Query( + "locale", + new VString(), + "Currently set locale (used to augment order by clause if necessary),", + required: false, + )] public function actionDefault( int $offset = 0, int $limit = null, @@ -177,11 +199,15 @@ function (Pipeline $pipeline) { /** * Create a brand new pipeline. * @POST - * @Param(type="post", name="global", validation="bool", required=false, - * description="Whether the pipeline is global (has no author, is used in generic runtimes)") * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Post( + "global", + new VBool(), + "Whether the pipeline is global (has no author, is used in generic runtimes)", + required: false, + )] public function actionCreatePipeline() { $req = $this->getRequest(); @@ -205,12 +231,16 @@ public function actionCreatePipeline() /** * Create a complete copy of given pipeline. * @POST - * @param string $id identification of pipeline to be copied - * @Param(type="post", name="global", validation="bool", required=false, - * description="Whether the pipeline is global (has no author, is used in generic runtimes)") * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Post( + "global", + new VBool(), + "Whether the pipeline is global (has no author, is used in generic runtimes)", + required: false, + )] + #[Path("id", new VString(), "identification of pipeline to be copied", required: true)] public function actionForkPipeline(string $id) { $req = $this->getRequest(); @@ -243,9 +273,9 @@ public function checkRemovePipeline(string $id) /** * Delete an pipeline * @DELETE - * @param string $id * @throws NotFoundException */ + #[Path("id", new VString(), required: true)] public function actionRemovePipeline(string $id) { $pipeline = $this->pipelines->findOrThrow($id); @@ -265,9 +295,9 @@ public function checkGetPipeline(string $id) /** * Get pipeline based on given identification. * @GET - * @param string $id Identifier of the pipeline * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the pipeline", required: true)] public function actionGetPipeline(string $id) { /** @var Pipeline $pipeline */ @@ -287,20 +317,24 @@ public function checkUpdatePipeline(string $id) /** * Update pipeline with given data. * @POST - * @param string $id Identifier of the pipeline - * @Param(type="post", name="name", validation="string:2..", description="Name of the pipeline") - * @Param(type="post", name="version", validation="numericint", description="Version of the edited pipeline") - * @Param(type="post", name="description", description="Human readable description of pipeline") - * @Param(type="post", name="pipeline", description="Pipeline configuration", required=false) - * @Param(type="post", name="parameters", validation="array", description="A set of parameters", required=false) - * @Param(type="post", name="global", validation="bool", required=false, - * description="Whether the pipeline is global (has no author, is used in generic runtimes)") * @throws ForbiddenRequestException * @throws NotFoundException * @throws BadRequestException * @throws ExerciseConfigException * @throws InvalidArgumentException */ + #[Post("name", new VString(2), "Name of the pipeline")] + #[Post("version", new VInt(), "Version of the edited pipeline")] + #[Post("description", new VString(), "Human readable description of pipeline")] + #[Post("pipeline", new VString(), "Pipeline configuration", required: false)] + #[Post("parameters", new VArray(), "A set of parameters", required: false)] + #[Post( + "global", + new VBool(), + "Whether the pipeline is global (has no author, is used in generic runtimes)", + required: false, + )] + #[Path("id", new VString(), "Identifier of the pipeline", required: true)] public function actionUpdatePipeline(string $id) { /** @var Pipeline $pipeline */ @@ -375,11 +409,11 @@ public function checkUpdateRuntimeEnvironments(string $id) /** * Set runtime environments associated with given pipeline. - * @param string $id Identifier of the pipeline * @POST * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the pipeline", required: true)] public function actionUpdateRuntimeEnvironments(string $id) { /** @var Pipeline $pipeline */ @@ -398,11 +432,11 @@ public function actionUpdateRuntimeEnvironments(string $id) /** * Check if the version of the pipeline is up-to-date. * @POST - * @Param(type="post", name="version", validation="numericint", description="Version of the pipeline.") - * @param string $id Identifier of the pipeline * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Post("version", new VInt(), "Version of the pipeline.")] + #[Path("id", new VString(), "Identifier of the pipeline", required: true)] public function actionValidatePipeline(string $id) { $pipeline = $this->pipelines->findOrThrow($id); @@ -432,12 +466,12 @@ public function checkUploadSupplementaryFiles(string $id) /** * Associate supplementary files with a pipeline and upload them to remote file server * @POST - * @Param(type="post", name="files", description="Identifiers of supplementary files") - * @param string $id identification of pipeline * @throws ForbiddenRequestException * @throws SubmissionFailedException * @throws NotFoundException */ + #[Post("files", new VString(), "Identifiers of supplementary files")] + #[Path("id", new VString(), "identification of pipeline", required: true)] public function actionUploadSupplementaryFiles(string $id) { $pipeline = $this->pipelines->findOrThrow($id); @@ -488,9 +522,9 @@ public function checkGetSupplementaryFiles(string $id) /** * Get list of all supplementary files for a pipeline * @GET - * @param string $id identification of pipeline * @throws NotFoundException */ + #[Path("id", new VString(), "identification of pipeline", required: true)] public function actionGetSupplementaryFiles(string $id) { $pipeline = $this->pipelines->findOrThrow($id); @@ -508,10 +542,10 @@ public function checkDeleteSupplementaryFile(string $id, string $fileId) /** * Delete supplementary pipeline file with given id * @DELETE - * @param string $id identification of pipeline - * @param string $fileId identification of file * @throws NotFoundException */ + #[Path("id", new VString(), "identification of pipeline", required: true)] + #[Path("fileId", new VString(), "identification of file", required: true)] public function actionDeleteSupplementaryFile(string $id, string $fileId) { $pipeline = $this->pipelines->findOrThrow($id); @@ -538,9 +572,9 @@ public function checkGetPipelineExercises(string $id) * Get all exercises that use given pipeline. * Only bare minimum is retrieved for each exercise (localized name and author). * @GET - * @param string $id Identifier of the pipeline * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the pipeline", required: true)] public function actionGetPipelineExercises(string $id) { $exercises = $this->exercises->getPipelineExercises($id); diff --git a/app/V1Module/presenters/PlagiarismPresenter.php b/app/V1Module/presenters/PlagiarismPresenter.php index 7b31d42cb..a928ec1f2 100644 --- a/app/V1Module/presenters/PlagiarismPresenter.php +++ b/app/V1Module/presenters/PlagiarismPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\ParseException; @@ -85,11 +97,19 @@ public function checkListBatches(?string $detectionTool, ?string $solutionId): v /** * Get a list of all batches, optionally filtered by query params. * @GET - * @Param(type="query", name="detectionTool", required=false, validation="string:1..255", - * description="Requests only batches created by a particular detection tool.") - * @Param(type="query", name="solutionId", required=false, validation="string:36", - * description="Requests only batches where particular solution has detected similarities.") */ + #[Query( + "detectionTool", + new VString(1, 255), + "Requests only batches created by a particular detection tool.", + required: false, + )] + #[Query( + "solutionId", + new VUuid(), + "Requests only batches where particular solution has detected similarities.", + required: false, + )] public function actionListBatches(?string $detectionTool, ?string $solutionId): void { $solution = $solutionId ? $this->assignmentSolutions->findOrThrow($solutionId) : null; @@ -108,6 +128,7 @@ public function checkBatchDetail(string $id): void * Fetch a detail of a particular batch record. * @GET */ + #[Path("id", new VString(), required: true)] public function actionBatchDetail(string $id): void { $batch = $this->detectionBatches->findOrThrow($id); @@ -124,11 +145,14 @@ public function checkCreateBatch(): void /** * Create new detection batch record * @POST - * @Param(type="post", name="detectionTool", validation="string:1..255", - * description="Identifier of the external tool used to detect similarities.") - * @Param(type="post", name="detectionToolParams", validation="string:0..255", required="false" - * description="Tool-specific parameters (e.g., CLI args) used for this particular batch.") */ + #[Post("detectionTool", new VString(1, 255), "Identifier of the external tool used to detect similarities.")] + #[Post( + "detectionToolParams", + new VString(0, 255), + "Tool-specific parameters (e.g., CLI args) used for this particular batch.", + required: false, + )] public function actionCreateBatch(): void { $req = $this->getRequest(); @@ -150,9 +174,9 @@ public function checkUpdateBatch(string $id): void /** * Update dectection bath record. At the momeny, only the uploadCompletedAt can be changed. * @POST - * @Param(type="post", name="uploadCompleted", validation="bool", - * description="Whether the upload of the batch data is completed or not.") */ + #[Post("uploadCompleted", new VBool(), "Whether the upload of the batch data is completed or not.")] + #[Path("id", new VString(), required: true)] public function actionUpdateBatch(string $id): void { $req = $this->getRequest(); @@ -176,6 +200,8 @@ public function checkGetSimilarities(string $id, string $solutionId): void * Returns a list of detected similarities entities (similar file records are nested within). * @GET */ + #[Path("id", new VString(), required: true)] + #[Path("solutionId", new VString(), required: true)] public function actionGetSimilarities(string $id, string $solutionId): void { $batch = $this->detectionBatches->findOrThrow($id); @@ -199,17 +225,19 @@ public function checkAddSimilarities(string $id, string $solutionId): void * Appends one detected similarity record (similarities associated with one file and one other author) * into a detected batch. This division was selected to make the appends relatively small and managable. * @POST - * @Param(type="post", name="solutionFileId", validation="string:36", - * description="Id of the uploaded solution file.") - * @Param(type="post", name="fileEntry", validation="string:0..255", required=false, - * description="Entry (relative path) within a ZIP package (if the uploaded file is a ZIP).") - * @Param(type="post", name="authorId", validation="string:36", - * description="Id of the author of the similar solutions/files.") - * @Param(type="post", name="similarity", validation="numeric", - * description="Relative similarity of the records associated with selected author [0-1].") - * @Param(type="post", name="files", validation="array", - * description="List of similar files and their records.") */ + #[Post("solutionFileId", new VUuid(), "Id of the uploaded solution file.")] + #[Post( + "fileEntry", + new VString(0, 255), + "Entry (relative path) within a ZIP package (if the uploaded file is a ZIP).", + required: false, + )] + #[Post("authorId", new VUuid(), "Id of the author of the similar solutions/files.")] + #[Post("similarity", new VFloat(), "Relative similarity of the records associated with selected author [0-1].")] + #[Post("files", new VArray(), "List of similar files and their records.")] + #[Path("id", new VString(), required: true)] + #[Path("solutionId", new VString(), required: true)] public function actionAddSimilarities(string $id, string $solutionId): void { $batch = $this->detectionBatches->findOrThrow($id); diff --git a/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php b/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php index 4fc1c94e9..9dd18ba3a 100644 --- a/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php +++ b/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Exceptions\ExerciseCompilationException; use App\Exceptions\ExerciseCompilationSoftException; @@ -170,8 +182,8 @@ public function checkSolutions(string $exerciseId) /** * Get reference solutions for an exercise * @GET - * @param string $exerciseId Identifier of the exercise */ + #[Path("exerciseId", new VString(), "Identifier of the exercise", required: true)] public function actionSolutions(string $exerciseId) { $exercise = $this->exercises->findOrThrow($exerciseId); @@ -200,9 +212,9 @@ public function checkDetail(string $solutionId) /** * Get details of a reference solution * @GET - * @param string $solutionId An identifier of the solution * @throws NotFoundException */ + #[Path("solutionId", new VString(), "An identifier of the solution", required: true)] public function actionDetail(string $solutionId) { $solution = $this->referenceSolutions->findOrThrow($solutionId); @@ -220,12 +232,11 @@ public function checkUpdate(string $solutionId) /** * Update details about the ref. solution (note, etc...) * @POST - * @Param(type="post", name="note", validation="string:0..65535", - * description="A description by the author of the solution") - * @param string $solutionId Identifier of the solution * @throws NotFoundException * @throws InternalServerException */ + #[Post("note", new VString(0, 65535), "A description by the author of the solution")] + #[Path("solutionId", new VString(), "Identifier of the solution", required: true)] public function actionUpdate(string $solutionId) { $req = $this->getRequest(); @@ -247,8 +258,8 @@ public function checkDeleteReferenceSolution(string $solutionId) /** * Delete reference solution with given identification. * @DELETE - * @param string $solutionId identifier of reference solution */ + #[Path("solutionId", new VString(), "identifier of reference solution", required: true)] public function actionDeleteReferenceSolution(string $solutionId) { $solution = $this->referenceSolutions->findOrThrow($solutionId); @@ -281,9 +292,9 @@ public function checkSubmissions(string $solutionId) /** * Get a list of submissions for given reference solution. * @GET - * @param string $solutionId identifier of the reference exercise solution * @throws InternalServerException */ + #[Path("solutionId", new VString(), "identifier of the reference exercise solution", required: true)] public function actionSubmissions(string $solutionId) { $solution = $this->referenceSolutions->findOrThrow($solutionId); @@ -308,10 +319,10 @@ public function checkSubmission(string $submissionId) /** * Get reference solution evaluation (i.e., submission) for an exercise solution. * @GET - * @param string $submissionId identifier of the reference exercise submission * @throws NotFoundException * @throws InternalServerException */ + #[Path("submissionId", new VString(), "identifier of the reference exercise submission", required: true)] public function actionSubmission(string $submissionId) { $submission = $this->referenceSubmissions->findOrThrow($submissionId); @@ -334,8 +345,8 @@ public function checkDeleteSubmission(string $submissionId) /** * Remove reference solution evaluation (submission) permanently. * @DELETE - * @param string $submissionId Identifier of the reference solution submission */ + #[Path("submissionId", new VString(), "Identifier of the reference solution submission", required: true)] public function actionDeleteSubmission(string $submissionId) { $submission = $this->referenceSubmissions->findOrThrow($submissionId); @@ -361,13 +372,13 @@ public function checkPreSubmit(string $exerciseId) * environments for the exercise. Also it can be further used for entry * points and other important things that should be provided by user during submit. * @POST - * @param string $exerciseId identifier of exercise - * @Param(type="post", name="files", validation="array", "Array of identifications of submitted files") * @throws NotFoundException * @throws InvalidArgumentException * @throws ExerciseConfigException * @throws BadRequestException */ + #[Post("files", new VArray())] + #[Path("exerciseId", new VString(), "identifier of exercise", required: true)] public function actionPreSubmit(string $exerciseId) { $exercise = $this->exercises->findOrThrow($exerciseId); @@ -413,18 +424,17 @@ public function checkSubmit(string $exerciseId) /** * Add new reference solution to an exercise * @POST - * @Param(type="post", name="note", validation="string", - * description="Description of this particular reference solution, for example used algorithm") - * @Param(type="post", name="files", description="Files of the reference solution") - * @Param(type="post", name="runtimeEnvironmentId", description="ID of runtime for this solution") - * @Param(type="post", name="solutionParams", required=false, description="Solution parameters") - * @param string $exerciseId Identifier of the exercise * @throws ForbiddenRequestException * @throws NotFoundException * @throws SubmissionEvaluationFailedException * @throws ParseException * @throws BadRequestException */ + #[Post("note", new VString(), "Description of this particular reference solution, for example used algorithm")] + #[Post("files", new VString(), "Files of the reference solution")] + #[Post("runtimeEnvironmentId", new VString(), "ID of runtime for this solution")] + #[Post("solutionParams", new VString(), "Solution parameters", required: false)] + #[Path("exerciseId", new VString(), "Identifier of the exercise", required: true)] public function actionSubmit(string $exerciseId) { $exercise = $this->exercises->findOrThrow($exerciseId); @@ -465,13 +475,12 @@ public function checkResubmit(string $id) /** * Evaluate a single reference exercise solution for all configured hardware groups * @POST - * @param string $id Identifier of the reference solution - * @Param(type="post", name="debug", validation="bool", required=false, - * description="Debugging evaluation with all logs and outputs") * @throws ForbiddenRequestException * @throws ParseException * @throws BadRequestException */ + #[Post("debug", new VBool(), "Debugging evaluation with all logs and outputs", required: false)] + #[Path("id", new VString(), "Identifier of the reference solution", required: true)] public function actionResubmit(string $id) { $req = $this->getRequest(); @@ -511,14 +520,13 @@ function ($solution) { /** * Evaluate all reference solutions for an exercise (and for all configured hardware groups). * @POST - * @param string $exerciseId Identifier of the exercise - * @Param(type="post", name="debug", validation="bool", required=false, - * description="Debugging evaluation with all logs and outputs") * @throws ForbiddenRequestException * @throws ParseException * @throws BadRequestException * @throws NotFoundException */ + #[Post("debug", new VBool(), "Debugging evaluation with all logs and outputs", required: false)] + #[Path("exerciseId", new VString(), "Identifier of the exercise", required: true)] public function actionResubmitAll($exerciseId) { $req = $this->getRequest(); @@ -610,11 +618,11 @@ public function checkDownloadSolutionArchive(string $solutionId) /** * Download archive containing all solution files for particular reference solution. * @GET - * @param string $solutionId of reference solution * @throws NotFoundException * @throws \Nette\Application\BadRequestException * @throws \Nette\Application\AbortException */ + #[Path("solutionId", new VString(), "of reference solution", required: true)] public function actionDownloadSolutionArchive(string $solutionId) { $solution = $this->referenceSolutions->findOrThrow($solutionId); @@ -636,10 +644,10 @@ public function checkFiles(string $id) /** * Get the list of submitted files of the solution. * @GET - * @param string $id of reference solution * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Path("id", new VString(), "of reference solution", required: true)] public function actionFiles(string $id) { $solution = $this->referenceSolutions->findOrThrow($id)->getSolution(); @@ -660,13 +668,13 @@ public function checkDownloadResultArchive(string $submissionId) /** * Download result archive from backend for a reference solution evaluation * @GET - * @param string $submissionId * @throws ForbiddenRequestException * @throws NotFoundException * @throws NotReadyException * @throws InternalServerException * @throws \Nette\Application\AbortException */ + #[Path("submissionId", new VString(), required: true)] public function actionDownloadResultArchive(string $submissionId) { $submission = $this->referenceSubmissions->findOrThrow($submissionId); @@ -695,10 +703,10 @@ public function checkEvaluationScoreConfig(string $submissionId) /** * Get score configuration associated with given submission evaluation * @GET - * @param string $submissionId identifier of the reference exercise submission * @throws NotFoundException * @throws InternalServerException */ + #[Path("submissionId", new VString(), "identifier of the reference exercise submission", required: true)] public function actionEvaluationScoreConfig(string $submissionId) { $submission = $this->referenceSubmissions->findOrThrow($submissionId); @@ -720,13 +728,12 @@ public function checkSetVisibility(string $solutionId) /** * Set visibility of given reference solution. * @POST - * @param string $solutionId of reference solution - * @Param(type="post", name="visibility", required=true, validation="numericint", - * description="New visibility level.") * @throws NotFoundException * @throws ForbiddenRequestException * @throws BadRequestException */ + #[Post("visibility", new VInt(), "New visibility level.", required: true)] + #[Path("solutionId", new VString(), "of reference solution", required: true)] public function actionSetVisibility(string $solutionId) { $solution = $this->referenceSolutions->findOrThrow($solutionId); diff --git a/app/V1Module/presenters/RegistrationPresenter.php b/app/V1Module/presenters/RegistrationPresenter.php index 971a36675..4cd07605c 100644 --- a/app/V1Module/presenters/RegistrationPresenter.php +++ b/app/V1Module/presenters/RegistrationPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\FrontendErrorMappings; use App\Exceptions\InvalidArgumentException; use App\Exceptions\WrongCredentialsException; @@ -148,23 +160,18 @@ public function checkCreateAccount() /** * Create a user account * @POST - * @Param(type="post", name="email", validation="email", description="An email that will serve as a login name") - * @Param(type="post", name="firstName", validation="string:2..", description="First name") - * @Param(type="post", name="lastName", validation="string:2..", description="Last name") - * @Param(type="post", name="password", validation="string:1..", msg="Password cannot be empty.", - * description="A password for authentication") - * @Param(type="post", name="passwordConfirm", validation="string:1..", msg="Confirm Password cannot be empty.", - * description="A password confirmation") - * @Param(type="post", name="instanceId", validation="string:1..", - * description="Identifier of the instance to register in") - * @Param(type="post", name="titlesBeforeName", required=false, validation="string:1..", - * description="Titles which is placed before user name") - * @Param(type="post", name="titlesAfterName", required=false, validation="string:1..", - * description="Titles which is placed after user name") * @throws BadRequestException * @throws WrongCredentialsException * @throws InvalidArgumentException */ + #[Post("email", new VEmail(), "An email that will serve as a login name")] + #[Post("firstName", new VString(2), "First name")] + #[Post("lastName", new VString(2), "Last name")] + #[Post("password", new VString(1), "A password for authentication")] + #[Post("passwordConfirm", new VString(1), "A password confirmation")] + #[Post("instanceId", new VString(1), "Identifier of the instance to register in")] + #[Post("titlesBeforeName", new VString(1), "Titles which is placed before user name", required: false)] + #[Post("titlesAfterName", new VString(1), "Titles which is placed after user name", required: false)] public function actionCreateAccount() { $req = $this->getRequest(); @@ -230,9 +237,9 @@ public function actionCreateAccount() /** * Check if the registered E-mail isn't already used and if the password is strong enough * @POST - * @Param(type="post", name="email", description="E-mail address (login name)") - * @Param(type="post", name="password", required=false, description="Authentication password") */ + #[Post("email", new VString(), "E-mail address (login name)")] + #[Post("password", new VString(), "Authentication password", required: false)] public function actionValidateRegistrationData() { $req = $this->getRequest(); @@ -263,22 +270,22 @@ public function checkCreateInvitation() /** * Create an invitation for a user and send it over via email * @POST - * @Param(type="post", name="email", validation="email", description="An email that will serve as a login name") - * @Param(type="post", name="firstName", required=true, validation="string:2..", description="First name") - * @Param(type="post", name="lastName", validation="string:2..", description="Last name") - * @Param(type="post", name="instanceId", validation="string:1..", - * description="Identifier of the instance to register in") - * @Param(type="post", name="titlesBeforeName", required=false, validation="string:1..", - * description="Titles which is placed before user name") - * @Param(type="post", name="titlesAfterName", required=false, validation="string:1..", - * description="Titles which is placed after user name") - * @Param(type="post", name="groups", required=false, validation="array", - * description="List of group IDs in which the user is added right after registration") - * @Param(type="post", name="locale", required=false, validation="string:2", - * description="Language used in the invitation email (en by default).") * @throws BadRequestException * @throws InvalidArgumentException */ + #[Post("email", new VEmail(), "An email that will serve as a login name")] + #[Post("firstName", new VString(2), "First name", required: true)] + #[Post("lastName", new VString(2), "Last name")] + #[Post("instanceId", new VString(1), "Identifier of the instance to register in")] + #[Post("titlesBeforeName", new VString(1), "Titles which is placed before user name", required: false)] + #[Post("titlesAfterName", new VString(1), "Titles which is placed after user name", required: false)] + #[Post( + "groups", + new VArray(), + "List of group IDs in which the user is added right after registration", + required: false, + )] + #[Post("locale", new VString(2, 2), "Language used in the invitation email (en by default).", required: false)] public function actionCreateInvitation() { $req = $this->getRequest(); @@ -332,15 +339,12 @@ public function actionCreateInvitation() /** * Accept invitation and create corresponding user account. * @POST - * @Param(type="post", name="token", validation="string:1..", - * description="Token issued in create invitation process.") - * @Param(type="post", name="password", validation="string:1..", msg="Password cannot be empty.", - * description="A password for authentication") - * @Param(type="post", name="passwordConfirm", validation="string:1..", msg="Confirm Password cannot be empty.", - * description="A password confirmation") * @throws BadRequestException * @throws InvalidArgumentException */ + #[Post("token", new VString(1), "Token issued in create invitation process.")] + #[Post("password", new VString(1), "A password for authentication")] + #[Post("passwordConfirm", new VString(1), "A password confirmation")] public function actionAcceptInvitation() { $req = $this->getRequest(); diff --git a/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php b/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php index fbb1cd57b..e087c90e2 100644 --- a/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php +++ b/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Model\Repository\RuntimeEnvironments; use App\Security\ACL\IRuntimeEnvironmentPermissions; diff --git a/app/V1Module/presenters/SecurityPresenter.php b/app/V1Module/presenters/SecurityPresenter.php index 476e41c33..436fc3a3a 100644 --- a/app/V1Module/presenters/SecurityPresenter.php +++ b/app/V1Module/presenters/SecurityPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\InvalidArgumentException; use Exception; use Nette\Application\IPresenterFactory; @@ -25,9 +37,9 @@ class SecurityPresenter extends BasePresenter /** * @POST - * @Param(name="url", type="post", required=true, description="URL of the resource that we are checking") - * @Param(name="method", type="post", required=true, description="The HTTP method") */ + #[Post("url", new VString(), "URL of the resource that we are checking", required: true)] + #[Post("method", new VString(), "The HTTP method", required: true)] public function actionCheck() { $requestParams = $this->router->match( diff --git a/app/V1Module/presenters/ShadowAssignmentsPresenter.php b/app/V1Module/presenters/ShadowAssignmentsPresenter.php index 927bb41c7..72cdf24c1 100644 --- a/app/V1Module/presenters/ShadowAssignmentsPresenter.php +++ b/app/V1Module/presenters/ShadowAssignmentsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\InvalidArgumentException; @@ -90,9 +102,9 @@ public function checkDetail(string $id) /** * Get details of a shadow assignment * @GET - * @param string $id Identifier of the assignment * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the assignment", required: true)] public function actionDetail(string $id) { $assignment = $this->shadowAssignments->findOrThrow($id); @@ -110,10 +122,10 @@ public function checkValidate(string $id) /** * Check if the version of the shadow assignment is up-to-date. * @POST - * @Param(type="post", name="version", validation="numericint", description="Version of the shadow assignment.") - * @param string $id Identifier of the shadow assignment * @throws ForbiddenRequestException */ + #[Post("version", new VInt(), "Version of the shadow assignment.")] + #[Path("id", new VString(), "Identifier of the shadow assignment", required: true)] public function actionValidate($id) { $assignment = $this->shadowAssignments->findOrThrow($id); @@ -136,25 +148,28 @@ public function checkUpdateDetail(string $id) /** * Update details of an shadow assignment * @POST - * @param string $id Identifier of the updated assignment - * @Param(type="post", name="version", validation="numericint", - * description="Version of the edited assignment") - * @Param(type="post", name="isPublic", validation="bool", - * description="Is the assignment ready to be displayed to students?") - * @Param(type="post", name="isBonus", validation="bool", - * description="If true, the points from this exercise will not be included in overall score of group") - * @Param(type="post", name="localizedTexts", validation="array", - * description="A description of the assignment") - * @Param(type="post", name="maxPoints", validation="numericint", - * description="A maximum of points that user can be awarded") - * @Param(type="post", name="sendNotification", required=false, validation="bool", - * description="If email notification should be sent") - * @Param(type="post", name="deadline", validation="timestamp|null", required=false, - * description="Deadline (only for visualization), missing value meas no deadline (same as null)") * @throws BadRequestException * @throws InvalidArgumentException * @throws NotFoundException */ + #[Post("version", new VInt(), "Version of the edited assignment")] + #[Post("isPublic", new VBool(), "Is the assignment ready to be displayed to students?")] + #[Post( + "isBonus", + new VBool(), + "If true, the points from this exercise will not be included in overall score of group", + )] + #[Post("localizedTexts", new VArray(), "A description of the assignment")] + #[Post("maxPoints", new VInt(), "A maximum of points that user can be awarded")] + #[Post("sendNotification", new VBool(), "If email notification should be sent", required: false)] + #[Post( + "deadline", + new VTimestamp(), + "Deadline (only for visualization), missing value meas no deadline (same as null)", + required: false, + nullable: true, + )] + #[Path("id", new VString(), "Identifier of the updated assignment", required: true)] public function actionUpdateDetail(string $id) { $assignment = $this->shadowAssignments->findOrThrow($id); @@ -242,11 +257,11 @@ public function actionUpdateDetail(string $id) /** * Create new shadow assignment in given group. * @POST - * @Param(type="post", name="groupId", description="Identifier of the group") * @throws ForbiddenRequestException * @throws BadRequestException * @throws NotFoundException */ + #[Post("groupId", new VString(), "Identifier of the group")] public function actionCreate() { $req = $this->getRequest(); @@ -277,9 +292,9 @@ public function checkRemove(string $id) /** * Delete shadow assignment * @DELETE - * @param string $id Identifier of the assignment to be removed * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the assignment to be removed", required: true)] public function actionRemove(string $id) { $assignment = $this->shadowAssignments->findOrThrow($id); @@ -298,18 +313,21 @@ public function checkCreatePoints(string $id) /** * Create new points for shadow assignment and user. * @POST - * @param string $id Identifier of the shadow assignment - * @Param(type="post", name="userId", validation="string", - * description="Identifier of the user which is marked as awardee for points") - * @Param(type="post", name="points", validation="numericint", description="Number of points assigned to the user") - * @Param(type="post", name="note", validation="string", description="Note about newly created points") - * @Param(type="post", name="awardedAt", validation="timestamp", required=false, - * description="Datetime when the points were awarded, whatever that means") * @throws NotFoundException * @throws ForbiddenRequestException * @throws BadRequestException * @throws InvalidStateException */ + #[Post("userId", new VString(), "Identifier of the user which is marked as awardee for points")] + #[Post("points", new VInt(), "Number of points assigned to the user")] + #[Post("note", new VString(), "Note about newly created points")] + #[Post( + "awardedAt", + new VTimestamp(), + "Datetime when the points were awarded, whatever that means", + required: false, + )] + #[Path("id", new VString(), "Identifier of the shadow assignment", required: true)] public function actionCreatePoints(string $id) { $req = $this->getRequest(); @@ -362,14 +380,18 @@ public function checkUpdatePoints(string $pointsId) /** * Update detail of shadow assignment points. * @POST - * @param string $pointsId Identifier of the shadow assignment points - * @Param(type="post", name="points", validation="numericint", description="Number of points assigned to the user") - * @Param(type="post", name="note", validation="string:0..1024", description="Note about newly created points") - * @Param(type="post", name="awardedAt", validation="timestamp", required=false, - * description="Datetime when the points were awarded, whatever that means") * @throws NotFoundException * @throws InvalidStateException */ + #[Post("points", new VInt(), "Number of points assigned to the user")] + #[Post("note", new VString(0, 1024), "Note about newly created points")] + #[Post( + "awardedAt", + new VTimestamp(), + "Datetime when the points were awarded, whatever that means", + required: false, + )] + #[Path("pointsId", new VString(), "Identifier of the shadow assignment points", required: true)] public function actionUpdatePoints(string $pointsId) { $pointsEntity = $this->shadowAssignmentPointsRepository->findOrThrow($pointsId); @@ -408,9 +430,9 @@ public function checkRemovePoints(string $pointsId) /** * Remove points of shadow assignment. * @DELETE - * @param string $pointsId Identifier of the shadow assignment points * @throws NotFoundException */ + #[Path("pointsId", new VString(), "Identifier of the shadow assignment points", required: true)] public function actionRemovePoints(string $pointsId) { $points = $this->shadowAssignmentPointsRepository->findOrThrow($pointsId); diff --git a/app/V1Module/presenters/SisPresenter.php b/app/V1Module/presenters/SisPresenter.php index fc132cc57..53aeaa127 100644 --- a/app/V1Module/presenters/SisPresenter.php +++ b/app/V1Module/presenters/SisPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ApiException; use App\Exceptions\BadRequestException; use App\Exceptions\ForbiddenRequestException; @@ -134,12 +146,12 @@ public function checkRegisterTerm() /** * Register a new term * @POST - * @Param(name="year", type="post") - * @Param(name="term", type="post") * @throws InvalidArgumentException * @throws ForbiddenRequestException * @throws BadRequestException */ + #[Post("year", new VString())] + #[Post("term", new VString())] public function actionRegisterTerm() { $year = intval($this->getRequest()->getPost("year")); @@ -170,13 +182,13 @@ public function checkEditTerm(string $id) /** * Set details of a term * @POST - * @Param(name="beginning", type="post", validation="timestamp") - * @Param(name="end", type="post", validation="timestamp") - * @Param(name="advertiseUntil", type="post", validation="timestamp") - * @param string $id * @throws InvalidArgumentException * @throws NotFoundException */ + #[Post("beginning", new VTimestamp())] + #[Post("end", new VTimestamp())] + #[Post("advertiseUntil", new VTimestamp())] + #[Path("id", new VString(), required: true)] public function actionEditTerm(string $id) { $term = $this->sisValidTerms->findOrThrow($id); @@ -219,9 +231,9 @@ public function checkDeleteTerm(string $id) /** * Delete a term * @DELETE - * @param string $id * @throws NotFoundException */ + #[Path("id", new VString(), required: true)] public function actionDeleteTerm(string $id) { $term = $this->sisValidTerms->findOrThrow($id); @@ -246,12 +258,12 @@ public function checkSubscribedGroups($userId, $year, $term) * Each course holds bound group IDs and group objects are returned in a separate array. * Whole ancestral closure of groups is returned, so the webapp may properly assemble hiarichial group names. * @GET - * @param string $userId - * @param int $year - * @param int $term * @throws InvalidArgumentException * @throws BadRequestException */ + #[Path("userId", new VString(), required: true)] + #[Path("year", new VInt(), required: true)] + #[Path("term", new VInt(), required: true)] public function actionSubscribedCourses($userId, $year, $term) { $user = $this->users->findOrThrow($userId); @@ -313,13 +325,13 @@ public function checkSupervisedCourses($userId, $year, $term) * Each course holds bound group IDs and group objects are returned in a separate array. * Whole ancestral closure of groups is returned, so the webapp may properly assemble hiarichial group names. * @GET - * @param string $userId - * @param int $year - * @param int $term * @throws InvalidArgumentException * @throws NotFoundException * @throws BadRequestException */ + #[Path("userId", new VString(), required: true)] + #[Path("year", new VInt(), required: true)] + #[Path("term", new VInt(), required: true)] public function actionSupervisedCourses($userId, $year, $term) { $user = $this->users->findOrThrow($userId); @@ -405,13 +417,13 @@ private function makeCaptionsUnique(array &$captions, Group $parentGroup) /** * Create a new group based on a SIS group * @POST - * @param string $courseId * @throws BadRequestException - * @Param(name="parentGroupId", type="post") * @throws ForbiddenRequestException * @throws InvalidArgumentException * @throws Exception */ + #[Post("parentGroupId", new VString())] + #[Path("courseId", new VString(), required: true)] public function actionCreateGroup($courseId) { $user = $this->getCurrentUser(); @@ -479,12 +491,12 @@ public function actionCreateGroup($courseId) /** * Bind an existing local group to a SIS group * @POST - * @param string $courseId * @throws ApiException * @throws ForbiddenRequestException * @throws BadRequestException - * @Param(name="groupId", type="post") */ + #[Post("groupId", new VString())] + #[Path("courseId", new VString(), required: true)] public function actionBindGroup($courseId) { $user = $this->getCurrentUser(); @@ -512,13 +524,13 @@ public function actionBindGroup($courseId) /** * Delete a binding between a local group and a SIS group * @DELETE - * @param string $courseId an identifier of a SIS course - * @param string $groupId an identifier of a local group * @throws BadRequestException * @throws ForbiddenRequestException * @throws InvalidArgumentException * @throws NotFoundException */ + #[Path("courseId", new VString(), "an identifier of a SIS course", required: true)] + #[Path("groupId", new VString(), "an identifier of a local group", required: true)] public function actionUnbindGroup($courseId, $groupId) { $user = $this->getCurrentUser(); @@ -542,11 +554,11 @@ public function actionUnbindGroup($courseId, $groupId) /** * Find groups that can be chosen as parents of a group created from given SIS group by current user * @GET - * @param string $courseId * @throws ApiException * @throws ForbiddenRequestException * @throws BadRequestException */ + #[Path("courseId", new VString(), required: true)] public function actionPossibleParents($courseId) { $sisUserId = $this->getSisUserIdOrThrow($this->getCurrentUser()); diff --git a/app/V1Module/presenters/SubmissionFailuresPresenter.php b/app/V1Module/presenters/SubmissionFailuresPresenter.php index f74aa8e65..1cf34aa65 100644 --- a/app/V1Module/presenters/SubmissionFailuresPresenter.php +++ b/app/V1Module/presenters/SubmissionFailuresPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Exceptions\ForbiddenRequestException; use App\Helpers\Notifications\FailureResolutionEmailsSender; @@ -84,8 +96,8 @@ public function checkDetail(string $id) /** * Get details of a failure * @GET - * @param string $id An identifier of the failure */ + #[Path("id", new VString(), "An identifier of the failure", required: true)] public function actionDetail(string $id) { $failure = $this->submissionFailures->findOrThrow($id); @@ -103,12 +115,10 @@ public function checkResolve(string $id) /** * Mark a submission failure as resolved * @POST - * @param string $id An identifier of the failure - * @Param(name="note", type="post", validation="string:0..255", required=false, - * description="Brief description of how the failure was resolved") - * @Param(name="sendEmail", type="post", validation="bool", - * description="True if email should be sent to the author of submission") */ + #[Post("note", new VString(0, 255), "Brief description of how the failure was resolved", required: false)] + #[Post("sendEmail", new VBool(), "True if email should be sent to the author of submission")] + #[Path("id", new VString(), "An identifier of the failure", required: true)] public function actionResolve(string $id) { $failure = $this->submissionFailures->findOrThrow($id); diff --git a/app/V1Module/presenters/SubmitPresenter.php b/app/V1Module/presenters/SubmitPresenter.php index 253a36537..1709f21a0 100644 --- a/app/V1Module/presenters/SubmitPresenter.php +++ b/app/V1Module/presenters/SubmitPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ExerciseCompilationException; use App\Exceptions\ExerciseCompilationSoftException; use App\Exceptions\ExerciseConfigException; @@ -207,11 +219,11 @@ public function checkCanSubmit(string $id, string $userId = null) /** * Check if the given user can submit solutions to the assignment * @GET - * @param string $id Identifier of the assignment - * @param string|null $userId Identification of the user * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the assignment", required: true)] + #[Query("userId", new VString(), "Identification of the user", required: false)] public function actionCanSubmit(string $id, string $userId = null) { $assignment = $this->assignments->findOrThrow($id); @@ -231,19 +243,17 @@ public function actionCanSubmit(string $id, string $userId = null) /** * Submit a solution of an assignment * @POST - * @Param(type="post", name="note", validation="string:0..1024", - * description="A note by the author of the solution") - * @Param(type="post", name="userId", required=false, description="Author of the submission") - * @Param(type="post", name="files", description="Submitted files") - * @Param(type="post", name="runtimeEnvironmentId", - * description="Identifier of the runtime environment used for evaluation") - * @Param(type="post", name="solutionParams", required=false, description="Solution parameters") - * @param string $id Identifier of the assignment * @throws ForbiddenRequestException * @throws InvalidArgumentException * @throws NotFoundException * @throws ParseException */ + #[Post("note", new VString(0, 1024), "A note by the author of the solution")] + #[Post("userId", new VString(), "Author of the submission", required: false)] + #[Post("files", new VString(), "Submitted files")] + #[Post("runtimeEnvironmentId", new VString(), "Identifier of the runtime environment used for evaluation")] + #[Post("solutionParams", new VString(), "Solution parameters", required: false)] + #[Path("id", new VString(), "Identifier of the assignment", required: true)] public function actionSubmit(string $id) { $this->assignments->beginTransaction(); @@ -340,14 +350,13 @@ public function checkResubmit(string $id) /** * Resubmit a solution (i.e., create a new submission) * @POST - * @param string $id Identifier of the solution - * @Param(type="post", name="debug", validation="bool", required=false, - * description="Debugging resubmit with all logs and outputs") * @throws ForbiddenRequestException * @throws InvalidArgumentException * @throws NotFoundException * @throws ParseException */ + #[Post("debug", new VBool(), "Debugging resubmit with all logs and outputs", required: false)] + #[Path("id", new VString(), "Identifier of the solution", required: true)] public function actionResubmit(string $id) { $req = $this->getRequest(); @@ -369,10 +378,10 @@ public function checkResubmitAllAsyncJobStatus(string $id) * Return a list of all pending resubmit async jobs associated with given assignment. * Under normal circumstances, the list shoul be either empty, or contian only one job. * @GET - * @param string $id Identifier of the assignment * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the assignment", required: true)] public function actionResubmitAllAsyncJobStatus(string $id) { $assignment = $this->assignments->findOrThrow($id); @@ -394,10 +403,10 @@ public function checkResubmitAll(string $id) * No job is started when there are pending resubmit jobs for the selected assignment. * Returns list of pending async jobs (same as GET call) * @POST - * @param string $id Identifier of the assignment * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the assignment", required: true)] public function actionResubmitAll(string $id) { $assignment = $this->assignments->findOrThrow($id); @@ -431,13 +440,13 @@ public function checkPreSubmit(string $id, string $userId = null) * points and other important things that should be provided by user during * submit. * @POST - * @param string $id identifier of assignment - * @param string|null $userId Identifier of the submission author * @throws ExerciseConfigException * @throws InvalidArgumentException * @throws NotFoundException - * @Param(type="post", name="files", validation="array", "Array of identifications of submitted files") */ + #[Post("files", new VArray())] + #[Path("id", new VString(), "identifier of assignment", required: true)] + #[Query("userId", new VString(), "Identifier of the submission author", required: false)] public function actionPreSubmit(string $id, string $userId = null) { $assignment = $this->assignments->findOrThrow($id); diff --git a/app/V1Module/presenters/UploadedFilesPresenter.php b/app/V1Module/presenters/UploadedFilesPresenter.php index e27b8023d..639df56b4 100644 --- a/app/V1Module/presenters/UploadedFilesPresenter.php +++ b/app/V1Module/presenters/UploadedFilesPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\CannotReceiveUploadedFileException; use App\Exceptions\BadRequestException; use App\Exceptions\ForbiddenRequestException; @@ -122,8 +134,8 @@ public function checkDetail(string $id) * Get details of a file * @GET * @LoggedIn - * @param string $id Identifier of the uploaded file */ + #[Path("id", new VString(), "Identifier of the uploaded file", required: true)] public function actionDetail(string $id) { $file = $this->uploadedFiles->findOrThrow($id); @@ -164,14 +176,22 @@ public function checkDownload(string $id, ?string $entry = null, ?string $simila /** * Download a file * @GET - * @param string $id Identifier of the file - * @Param(type="query", name="entry", required=false, validation="string:1..", - * description="Name of the entry in the ZIP archive (if the target file is ZIP)") - * @Param(type="query", name="similarSolutionId", required=false, validation="string:36", - * description="Id of an assignment solution which has detected possible plagiarism in this file. This is basically a shortcut (hint) for ACLs.") * @throws \Nette\Application\AbortException * @throws \Nette\Application\BadRequestException */ + #[Query( + "entry", + new VString(1), + "Name of the entry in the ZIP archive (if the target file is ZIP)", + required: false, + )] + #[Query( + "similarSolutionId", + new VUuid(), + "Id of an assignment solution which has detected possible plagiarism in this file. This is basically a shortcut (hint) for ACLs.", + required: false, + )] + #[Path("id", new VString(), "Identifier of the file", required: true)] public function actionDownload(string $id, ?string $entry = null) { $fileEntity = $this->uploadedFiles->findOrThrow($id); @@ -205,12 +225,20 @@ public function checkContent(string $id, ?string $entry = null, ?string $similar /** * Get the contents of a file * @GET - * @param string $id Identifier of the file - * @Param(type="query", name="entry", required=false, validation="string:1..", - * description="Name of the entry in the ZIP archive (if the target file is ZIP)") - * @Param(type="query", name="similarSolutionId", required=false, validation="string:36", - * description="Id of an assignment solution which has detected possible plagiarism in this file. This is basically a shortcut (hint) for ACLs.") */ + #[Query( + "entry", + new VString(1), + "Name of the entry in the ZIP archive (if the target file is ZIP)", + required: false, + )] + #[Query( + "similarSolutionId", + new VUuid(), + "Id of an assignment solution which has detected possible plagiarism in this file. This is basically a shortcut (hint) for ACLs.", + required: false, + )] + #[Path("id", new VString(), "Identifier of the file", required: true)] public function actionContent(string $id, ?string $entry = null) { $fileEntity = $this->uploadedFiles->findOrThrow($id); @@ -264,8 +292,8 @@ public function checkDigest(string $id) * Compute a digest using a hashing algorithm. This feature is intended for upload checksums only. * In the future, we might want to add algorithm selection via query parameter (default is SHA1). * @GET - * @param string $id Identifier of the file */ + #[Path("id", new VString(), "Identifier of the file", required: true)] public function actionDigest(string $id) { $fileEntity = $this->uploadedFiles->findOrThrow($id); @@ -360,10 +388,9 @@ public function checkStartPartial() * each one carrying a chunk of data. Once all the chunks are in place, the complete request assembles * them together in one file and transforms UploadPartialFile into UploadFile entity. * @POST - * @Param(type="post", name="name", required=true, validation="string:1..255", - * description="Name of the uploaded file.") - * @Param(type="post", name="size", required=true, validation="numericint", description="Total size in bytes.") */ + #[Post("name", new VString(1, 255), "Name of the uploaded file.", required: true)] + #[Post("size", new VInt(), "Total size in bytes.", required: true)] public function actionStartPartial() { $user = $this->getCurrentUser(); @@ -408,15 +435,14 @@ public function checkAppendPartial(string $id) /** * Add another chunk to partial upload. * @PUT - * @param string $id Identifier of the file - * @Param(type="query", name="offset", required="true", validation="numericint", - * description="Offset of the chunk for verification") * @throws InvalidArgumentException * @throws ForbiddenRequestException * @throws BadRequestException * @throws CannotReceiveUploadedFileException * @throws InternalServerException */ + #[Query("offset", new VInt(), "Offset of the chunk for verification", required: true)] + #[Path("id", new VString(), "Identifier of the file", required: true)] public function actionAppendPartial(string $id, int $offset) { $partialFile = $this->uploadedPartialFiles->findOrThrow($id); @@ -461,6 +487,7 @@ public function checkCancelPartial(string $id) * Cancel partial upload and remove all uploaded chunks. * @DELETE */ + #[Path("id", new VString(), required: true)] public function actionCancelPartial(string $id) { $partialFile = $this->uploadedPartialFiles->findOrThrow($id); @@ -504,6 +531,7 @@ public function checkCompletePartial(string $id) * All data chunks are extracted from the store, assembled into one file, and is moved back into the store. * @POST */ + #[Path("id", new VString(), required: true)] public function actionCompletePartial(string $id) { $partialFile = $this->uploadedPartialFiles->findOrThrow($id); @@ -565,11 +593,11 @@ public function checkDownloadSupplementaryFile(string $id) /** * Download supplementary file * @GET - * @param string $id Identifier of the file * @throws ForbiddenRequestException * @throws NotFoundException * @throws \Nette\Application\AbortException */ + #[Path("id", new VString(), "Identifier of the file", required: true)] public function actionDownloadSupplementaryFile(string $id) { $fileEntity = $this->supplementaryFiles->findOrThrow($id); diff --git a/app/V1Module/presenters/UserCalendarsPresenter.php b/app/V1Module/presenters/UserCalendarsPresenter.php index e8373a526..adaa2be91 100644 --- a/app/V1Module/presenters/UserCalendarsPresenter.php +++ b/app/V1Module/presenters/UserCalendarsPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\BadRequestException; use App\Exceptions\ForbiddenRequestException; use App\Model\Entity\User; @@ -116,8 +128,8 @@ private function createDeadlineEvent(User $user, Assignment $assignment, string /** * Get calendar values in iCal format that correspond to given token. * @GET - * @param string $id the iCal token */ + #[Path("id", new VString(), "the iCal token", required: true)] public function actionDefault(string $id) { $calendar = $this->userCalendars->findOrThrow($id); @@ -179,8 +191,8 @@ public function checkUserCalendars(string $id) /** * Get all iCal tokens of one user (including expired ones). * @GET - * @param string $id of the user */ + #[Path("id", new VString(), "of the user", required: true)] public function actionUserCalendars(string $id) { $user = $this->users->findOrThrow($id); @@ -199,8 +211,8 @@ public function checkCreateCalendar(string $id) /** * Create new iCal token for a particular user. * @POST - * @param string $id of the user */ + #[Path("id", new VString(), "of the user", required: true)] public function actionCreateCalendar(string $id) { $user = $this->users->findOrThrow($id); @@ -221,8 +233,8 @@ public function checkExpireCalendar(string $id) /** * Set given iCal token to expired state. Expired tokens cannot be used to retrieve calendars. * @DELETE - * @param string $id the iCal token */ + #[Path("id", new VString(), "the iCal token", required: true)] public function actionExpireCalendar(string $id) { $calendar = $this->userCalendars->findOrThrow($id); diff --git a/app/V1Module/presenters/UsersPresenter.php b/app/V1Module/presenters/UsersPresenter.php index bc9c29485..a0bf06ca8 100644 --- a/app/V1Module/presenters/UsersPresenter.php +++ b/app/V1Module/presenters/UsersPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Exceptions\ForbiddenRequestException; use App\Exceptions\FrontendErrorMappings; use App\Exceptions\InvalidArgumentException; @@ -110,12 +122,22 @@ public function checkDefault() * Get a list of all users matching given filters in given pagination rage. * The result conforms to pagination protocol. * @GET - * @param int $offset Index of the first result. - * @param int|null $limit Maximal number of results returned. - * @param string|null $orderBy Name of the column (column concept). The '!' prefix indicate descending order. - * @param array|null $filters Named filters that prune the result. - * @param string|null $locale Currently set locale (used to augment order by clause if necessary), */ + #[Query("offset", new VInt(), "Index of the first result.", required: false)] + #[Query("limit", new VInt(), "Maximal number of results returned.", required: false)] + #[Query( + "orderBy", + new VString(), + "Name of the column (column concept). The '!' prefix indicate descending order.", + required: false, + )] + #[Query("filters", new VArray(), "Named filters that prune the result.", required: false)] + #[Query( + "locale", + new VString(), + "Currently set locale (used to augment order by clause if necessary),", + required: false, + )] public function actionDefault( int $offset = 0, int $limit = null, @@ -151,8 +173,8 @@ public function checkListByIds() /** * Get a list of users based on given ids. * @POST - * @Param(type="post", name="ids", validation="array", description="Identifications of users") */ + #[Post("ids", new VArray(), "Identifications of users")] public function actionListByIds() { $users = $this->users->findByIds($this->getRequest()->getPost("ids")); @@ -176,8 +198,8 @@ public function checkDetail(string $id) /** * Get details of a user account * @GET - * @param string $id Identifier of the user */ + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionDetail(string $id) { $user = $this->users->findOrThrow($id); @@ -195,9 +217,9 @@ public function checkDelete(string $id) /** * Delete a user account * @DELETE - * @param string $id Identifier of the user * @throws ForbiddenRequestException */ + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionDelete(string $id) { $user = $this->users->findOrThrow($id); @@ -218,26 +240,22 @@ public function checkUpdateProfile(string $id) /** * Update the profile associated with a user account * @POST - * @param string $id Identifier of the user * @throws BadRequestException * @throws ForbiddenRequestException * @throws InvalidArgumentException - * @Param(type="post", name="firstName", required=false, validation="string:2..", description="First name") - * @Param(type="post", name="lastName", required=false, validation="string:2..", description="Last name") - * @Param(type="post", name="titlesBeforeName", required=false, description="Titles before name") - * @Param(type="post", name="titlesAfterName", required=false, description="Titles after name") - * @Param(type="post", name="email", validation="email", description="New email address", required=false) - * @Param(type="post", name="oldPassword", required=false, validation="string:1..", - * description="Old password of current user") - * @Param(type="post", name="password", required=false, validation="string:1..", - * description="New password of current user") - * @Param(type="post", name="passwordConfirm", required=false, validation="string:1..", - * description="Confirmation of new password of current user") - * @Param(type="post", name="gravatarUrlEnabled", validation="bool", required=false, - * description="Enable or disable gravatar profile image") * @throws WrongCredentialsException * @throws NotFoundException */ + #[Post("firstName", new VString(2), "First name", required: false)] + #[Post("lastName", new VString(2), "Last name", required: false)] + #[Post("titlesBeforeName", new VString(), "Titles before name", required: false)] + #[Post("titlesAfterName", new VString(), "Titles after name", required: false)] + #[Post("email", new VEmail(), "New email address", required: false)] + #[Post("oldPassword", new VString(1), "Old password of current user", required: false)] + #[Post("password", new VString(1), "New password of current user", required: false)] + #[Post("passwordConfirm", new VString(1), "Confirmation of new password of current user", required: false)] + #[Post("gravatarUrlEnabled", new VBool(), "Enable or disable gravatar profile image", required: false)] + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionUpdateProfile(string $id) { $req = $this->getRequest(); @@ -429,33 +447,76 @@ public function checkUpdateSettings(string $id) /** * Update the profile settings * @POST - * @param string $id Identifier of the user - * @Param(type="post", name="defaultLanguage", validation="string", required=false, - * description="Default language of UI") - * @Param(type="post", name="newAssignmentEmails", validation="bool", required=false, - * description="Flag if email should be sent to user when new assignment was created") - * @Param(type="post", name="assignmentDeadlineEmails", validation="bool", required=false, - * description="Flag if email should be sent to user if assignment deadline is nearby") - * @Param(type="post", name="submissionEvaluatedEmails", validation="bool", required=false, - * description="Flag if email should be sent to user when resubmission was evaluated") - * @Param(type="post", name="solutionCommentsEmails", validation="bool", required=false, - * description="Flag if email should be sent to user when new submission comment is added") - * @Param(type="post", name="solutionReviewsEmails", validation="bool", required=false, - * description="Flag enabling review-related email notifications sent to the author of the solution") - * @Param(type="post", name="pointsChangedEmails", validation="bool", required=false, - * description="Flag if email should be sent to user when the points were awarded for assignment") - * @Param(type="post", name="assignmentSubmitAfterAcceptedEmails", validation="bool", required=false, - * description="Flag if email should be sent to group supervisor if a student submits new solution for already accepted assignment") - * @Param(type="post", name="assignmentSubmitAfterReviewedEmails", validation="bool", required=false, - * description="Flag if email should be sent to group supervisor if a student submits new solution for already reviewed and not accepted assignment") - * @Param(type="post", name="exerciseNotificationEmails", validation="bool", required=false, - * description="Flag if notifications sent by authors of exercises should be sent via email.") - * @Param(type="post", name="solutionAcceptedEmails", validation="bool", required=false, - * description="Flag if notification should be sent to a student when solution accepted flag is changed.") - * @Param(type="post", name="solutionReviewRequestedEmails", validation="bool", required=false, - * description="Flag if notification should be send to a teacher when a solution reviewRequested flag is chagned in a supervised/admined group.") * @throws NotFoundException */ + #[Post("defaultLanguage", new VString(), "Default language of UI", required: false)] + #[Post( + "newAssignmentEmails", + new VBool(), + "Flag if email should be sent to user when new assignment was created", + required: false, + )] + #[Post( + "assignmentDeadlineEmails", + new VBool(), + "Flag if email should be sent to user if assignment deadline is nearby", + required: false, + )] + #[Post( + "submissionEvaluatedEmails", + new VBool(), + "Flag if email should be sent to user when resubmission was evaluated", + required: false, + )] + #[Post( + "solutionCommentsEmails", + new VBool(), + "Flag if email should be sent to user when new submission comment is added", + required: false, + )] + #[Post( + "solutionReviewsEmails", + new VBool(), + "Flag enabling review-related email notifications sent to the author of the solution", + required: false, + )] + #[Post( + "pointsChangedEmails", + new VBool(), + "Flag if email should be sent to user when the points were awarded for assignment", + required: false, + )] + #[Post( + "assignmentSubmitAfterAcceptedEmails", + new VBool(), + "Flag if email should be sent to group supervisor if a student submits new solution for already accepted assignment", + required: false, + )] + #[Post( + "assignmentSubmitAfterReviewedEmails", + new VBool(), + "Flag if email should be sent to group supervisor if a student submits new solution for already reviewed and not accepted assignment", + required: false, + )] + #[Post( + "exerciseNotificationEmails", + new VBool(), + "Flag if notifications sent by authors of exercises should be sent via email.", + required: false, + )] + #[Post( + "solutionAcceptedEmails", + new VBool(), + "Flag if notification should be sent to a student when solution accepted flag is changed.", + required: false, + )] + #[Post( + "solutionReviewRequestedEmails", + new VBool(), + "Flag if notification should be send to a teacher when a solution reviewRequested flag is chagned in a supervised/admined group.", + required: false, + )] + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionUpdateSettings(string $id) { $req = $this->getRequest(); @@ -505,12 +566,16 @@ public function checkUpdateUiData(string $id) /** * Update the user-specific structured UI data * @POST - * @param string $id Identifier of the user - * @Param(type="post", name="uiData", validation="array|null", description="Structured user-specific UI data") - * @Param(type="post", name="overwrite", validation="bool", required=false, - * description="Flag indicating that uiData should be overwritten completelly (instead of regular merge)") * @throws NotFoundException */ + #[Post("uiData", new VArray(), "Structured user-specific UI data", nullable: true)] + #[Post( + "overwrite", + new VBool(), + "Flag indicating that uiData should be overwritten completelly (instead of regular merge)", + required: false, + )] + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionUpdateUiData(string $id) { $req = $this->getRequest(); @@ -564,9 +629,9 @@ public function checkCreateLocalAccount(string $id) * If user is registered externally, add local account as another login method. * Created password is empty and has to be changed in order to use it. * @POST - * @param string $id * @throws InvalidArgumentException */ + #[Path("id", new VString(), required: true)] public function actionCreateLocalAccount(string $id) { $user = $this->users->findOrThrow($id); @@ -588,8 +653,8 @@ public function checkGroups(string $id) /** * Get a list of non-archived groups for a user * @GET - * @param string $id Identifier of the user */ + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionGroups(string $id) { $user = $this->users->findOrThrow($id); @@ -631,8 +696,8 @@ public function checkAllGroups(string $id) /** * Get a list of all groups for a user * @GET - * @param string $id Identifier of the user */ + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionAllGroups(string $id) { $user = $this->users->findOrThrow($id); @@ -656,9 +721,9 @@ public function checkInstances(string $id) /** * Get a list of instances where a user is registered * @GET - * @param string $id Identifier of the user * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionInstances(string $id) { $user = $this->users->findOrThrow($id); @@ -684,12 +749,11 @@ public function checkSetRole(string $id) /** * Set a given role to the given user. * @POST - * @param string $id Identifier of the user - * @Param(type="post", name="role", validation="string:1..", - * description="Role which should be assigned to the user") * @throws InvalidArgumentException * @throws NotFoundException */ + #[Post("role", new VString(1), "Role which should be assigned to the user")] + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionSetRole(string $id) { $user = $this->users->findOrThrow($id); @@ -716,10 +780,10 @@ public function checkInvalidateTokens(string $id) /** * Invalidate all existing tokens issued for given user * @POST - * @param string $id Identifier of the user * @throws ForbiddenRequestException * @throws NotFoundException */ + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionInvalidateTokens(string $id) { $user = $this->users->findOrThrow($id); @@ -755,12 +819,11 @@ public function checkSetAllowed(string $id) /** * Set "isAllowed" flag of the given user. The flag determines whether a user may perform any operation of the API. * @POST - * @param string $id Identifier of the user - * @Param(type="post", name="isAllowed", validation="bool", - * description="Whether the user is allowed (active) or not.") * @throws InvalidArgumentException * @throws NotFoundException */ + #[Post("isAllowed", new VBool(), "Whether the user is allowed (active) or not.")] + #[Path("id", new VString(), "Identifier of the user", required: true)] public function actionSetAllowed(string $id) { $user = $this->users->findOrThrow($id); @@ -783,11 +846,11 @@ public function checkUpdateExternalLogin(string $id, string $service) /** * Add or update existing external ID of given authentication service. * @POST - * @param string $id identifier of the user - * @param string $service identifier of the authentication service (login type) - * @Param(type="post", name="externalId", validation="string:1..128") * @throws InvalidArgumentException */ + #[Post("externalId", new VString(1, 128))] + #[Path("id", new VString(), "identifier of the user", required: true)] + #[Path("service", new VString(), "identifier of the authentication service (login type)", required: true)] public function actionUpdateExternalLogin(string $id, string $service) { $user = $this->users->findOrThrow($id); @@ -830,9 +893,9 @@ public function checkRemoveExternalLogin(string $id, string $service) /** * Remove external ID of given authentication service. * @DELETE - * @param string $id identifier of the user - * @param string $service identifier of the authentication service (login type) */ + #[Path("id", new VString(), "identifier of the user", required: true)] + #[Path("service", new VString(), "identifier of the authentication service (login type)", required: true)] public function actionRemoveExternalLogin(string $id, string $service) { $user = $this->users->findOrThrow($id); diff --git a/app/V1Module/presenters/WorkerFilesPresenter.php b/app/V1Module/presenters/WorkerFilesPresenter.php index 471073fe9..65f78762c 100644 --- a/app/V1Module/presenters/WorkerFilesPresenter.php +++ b/app/V1Module/presenters/WorkerFilesPresenter.php @@ -2,6 +2,18 @@ namespace App\V1Module\Presenters; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Type; +use App\Helpers\MetaFormats\Validators\VArray; +use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VEmail; +use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VTimestamp; +use App\Helpers\MetaFormats\Validators\VUuid; use App\Helpers\BasicAuthHelper; use App\Helpers\WorkerFilesConfig; use App\Helpers\FileStorageManager; @@ -91,9 +103,9 @@ public function startup() * Sends over a ZIP file containing submitted files and YAML job config. * The ZIP is created if necessary. * @GET - * @param string $type of the submission job ("reference" or "student") - * @param string $id of the submission whose ZIP archive is to be served */ + #[Path("type", new VString(), "of the submission job (\"reference\" or \"student\")", required: true)] + #[Path("id", new VString(), "of the submission whose ZIP archive is to be served", required: true)] public function actionDownloadSubmissionArchive(string $type, string $id) { $file = $this->fileStorage->getWorkerSubmissionArchive($type, $id); @@ -112,8 +124,8 @@ public function actionDownloadSubmissionArchive(string $type, string $id) /** * Sends over an exercise supplementary file (a data file required by the tests). * @GET - * @param string $hash identification of the supplementary file */ + #[Path("hash", new VString(), "identification of the supplementary file", required: true)] public function actionDownloadSupplementaryFile(string $hash) { $file = $this->fileStorage->getSupplementaryFileByHash($hash); @@ -126,10 +138,10 @@ public function actionDownloadSupplementaryFile(string $hash) /** * Uploads a ZIP archive with results and logs (or everything in case of debug evaluations). * @PUT - * @param string $type of the submission job ("reference" or "student") - * @param string $id of the submission whose results archive is being uploaded * @throws UploadedFileException */ + #[Path("type", new VString(), "of the submission job (\"reference\" or \"student\")", required: true)] + #[Path("id", new VString(), "of the submission whose results archive is being uploaded", required: true)] public function actionUploadResultsFile(string $type, string $id) { try { From 1ea5bc1c788ad0c0b18a9bbb7abdc471c0b521c0 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 13:01:55 +0100 Subject: [PATCH 044/103] bugfix: attributes derived from Param are now also considered for validation purposes --- app/helpers/MetaFormats/MetaFormatHelper.php | 21 ++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 778150fd5..65bef3dd7 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -6,6 +6,9 @@ use App\Helpers\MetaFormats\Attributes\FormatAttribute; use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute; use App\Helpers\MetaFormats\Attributes\Param; +use App\Helpers\MetaFormats\Attributes\Path; +use App\Helpers\MetaFormats\Attributes\Post; +use App\Helpers\MetaFormats\Attributes\Query; use ReflectionClass; use App\Helpers\Swagger\AnnotationHelper; use ReflectionMethod; @@ -59,6 +62,20 @@ public static function extractFormatFromAttribute( return $formatAttribute->class; } + /** + * Extracts all endpoint parameter attributes. + * @param \ReflectionMethod $reflectionMethod The endpoint reflection method. + * @return array Returns an array of parameter attributes. + */ + public static function getEndpointAttributes(ReflectionMethod $reflectionMethod): array + { + $path = $reflectionMethod->getAttributes(name: Path::class); + $query = $reflectionMethod->getAttributes(name: Query::class); + $post = $reflectionMethod->getAttributes(name: Post::class); + $param = $reflectionMethod->getAttributes(name: Param::class); + return array_merge($path, $query, $post, $param); + } + /** * Fetches all attributes of a method and extracts the parameter data. * @param \ReflectionMethod $reflectionMethod The method reflection object. @@ -66,7 +83,7 @@ public static function extractFormatFromAttribute( */ public static function extractRequestParamData(ReflectionMethod $reflectionMethod): array { - $attrs = $reflectionMethod->getAttributes(Param::class); + $attrs = self::getEndpointAttributes($reflectionMethod); $data = []; foreach ($attrs as $attr) { $paramAttr = $attr->newInstance(); @@ -108,7 +125,7 @@ public static function extractFormatParameterData(ReflectionProperty $reflection * in the code. Each element is an instance of the specific attribute. */ public static function debugGetAttributes( - ReflectionClass|ReflectionProperty|ReflectionMethod $reflectionObject + ReflectionClass | ReflectionProperty | ReflectionMethod $reflectionObject ): array { $requestAttributes = $reflectionObject->getAttributes(); $data = []; From 99a41b8d99c55330a8ad4b9def889abdb8ceef57 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 13:08:22 +0100 Subject: [PATCH 045/103] bugfix: InvalidArgumentExceptions are no longer intercepted. --- app/helpers/MetaFormats/RequestParamData.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index a96772c5e..bb97c680b 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -73,10 +73,8 @@ public function conformsToDefinition(mixed $value) throw new InvalidArgumentException($this->name); } } - } catch (Exception $e) { - throw new InternalServerException( - "The validator of parameter {$this->name} is corrupted. Parameter description: {$this->description}" - ); + } catch (InternalServerException $e) { + throw $e; } return true; From 8c14400b6920c41c19004e2986cf4c3befdd98db Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 13:09:45 +0100 Subject: [PATCH 046/103] removed unnecessary rethrow mechanism --- app/helpers/MetaFormats/RequestParamData.php | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index bb97c680b..50db5fe89 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -62,19 +62,11 @@ public function conformsToDefinition(mixed $value) return true; } - ///TODO: check whether this works (test the internal exception as well) - // apply validators - // if an unexpected error is thrown, it is likely that the validator does not conform to the validator - // interface - try { - // use every provided validator - foreach ($this->validators as $validator) { - if (!$validator->validate($value)) { - throw new InvalidArgumentException($this->name); - } + // use every provided validator + foreach ($this->validators as $validator) { + if (!$validator->validate($value)) { + throw new InvalidArgumentException($this->name); } - } catch (InternalServerException $e) { - throw $e; } return true; From 2a57deee22bf1c37f79f69f31d539d084853f2e7 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 13:18:38 +0100 Subject: [PATCH 047/103] actionValidatePasswordStrength password is now nullable --- app/V1Module/presenters/ForgottenPasswordPresenter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/V1Module/presenters/ForgottenPasswordPresenter.php b/app/V1Module/presenters/ForgottenPasswordPresenter.php index a85acc6a2..a000f09d1 100644 --- a/app/V1Module/presenters/ForgottenPasswordPresenter.php +++ b/app/V1Module/presenters/ForgottenPasswordPresenter.php @@ -110,7 +110,7 @@ public function actionChange() * Check if a password is strong enough * @POST */ - #[Post("password", new VString(), "The password to be checked")] + #[Post("password", new VString(), "The password to be checked", nullable: true)] public function actionValidatePasswordStrength() { $password = $this->getRequest()->getPost("password"); From b05b6bf78f35b3a6eefd028be5a2a39ed4e0a371 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 13:25:28 +0100 Subject: [PATCH 048/103] added mixed placeholder validator --- .../NetteAnnotationConverter.php | 8 +++++-- app/helpers/MetaFormats/Validators/VMixed.php | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 app/helpers/MetaFormats/Validators/VMixed.php diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index 7c8d0f529..c02a95132 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -8,6 +8,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -196,6 +197,9 @@ private static function convertAnnotationValidationToValidatorString(string $val case "numeric": $validatorClass = VFloat::class; break; + case "mixed": + $validatorClass = VMixed::class; + break; default: throw new InternalServerException("Unknown validation rule: $validation"); } @@ -221,9 +225,9 @@ public static function convertRegexCapturesToParenthesesBuilder(array $annotatio } $parenthesesBuilder->addValue("\"{$annotationParameters["name"]}\""); - // replace missing validations with string validations + // replace missing validations with placeholder validations if (!array_key_exists("validation", $annotationParameters)) { - $annotationParameters["validation"] = "string"; + $annotationParameters["validation"] = "mixed"; } $nullable = false; $validation = $annotationParameters["validation"]; diff --git a/app/helpers/MetaFormats/Validators/VMixed.php b/app/helpers/MetaFormats/Validators/VMixed.php new file mode 100644 index 000000000..04b7fee3c --- /dev/null +++ b/app/helpers/MetaFormats/Validators/VMixed.php @@ -0,0 +1,24 @@ + Date: Thu, 20 Feb 2025 13:31:40 +0100 Subject: [PATCH 049/103] added VMixed validator as a placeholder for params with missing validations --- .../AssignmentSolutionReviewsPresenter.php | 1 + .../AssignmentSolutionsPresenter.php | 9 ++++++- .../presenters/AssignmentSolversPresenter.php | 1 + .../presenters/AssignmentsPresenter.php | 5 ++-- .../presenters/AsyncJobsPresenter.php | 1 + app/V1Module/presenters/BrokerPresenter.php | 1 + .../presenters/BrokerReportsPresenter.php | 7 +++--- app/V1Module/presenters/CommentsPresenter.php | 1 + app/V1Module/presenters/DefaultPresenter.php | 1 + .../presenters/EmailVerificationPresenter.php | 1 + app/V1Module/presenters/EmailsPresenter.php | 1 + .../presenters/ExerciseFilesPresenter.php | 5 ++-- .../presenters/ExercisesConfigPresenter.php | 4 ++- .../presenters/ExercisesPresenter.php | 12 ++++++--- .../presenters/ExtensionsPresenter.php | 1 + .../presenters/ForgottenPasswordPresenter.php | 3 ++- .../GroupExternalAttributesPresenter.php | 1 + .../presenters/GroupInvitationsPresenter.php | 5 ++-- app/V1Module/presenters/GroupsPresenter.php | 25 ++++++++++++++----- .../presenters/HardwareGroupsPresenter.php | 1 + .../presenters/InstancesPresenter.php | 3 ++- app/V1Module/presenters/LoginPresenter.php | 1 + .../presenters/NotificationsPresenter.php | 1 + .../presenters/PipelinesPresenter.php | 7 +++--- .../presenters/PlagiarismPresenter.php | 1 + .../ReferenceExerciseSolutionsPresenter.php | 7 +++--- .../presenters/RegistrationPresenter.php | 5 ++-- .../RuntimeEnvironmentsPresenter.php | 1 + app/V1Module/presenters/SecurityPresenter.php | 5 ++-- .../presenters/ShadowAssignmentsPresenter.php | 3 ++- app/V1Module/presenters/SisPresenter.php | 9 ++++--- .../SubmissionFailuresPresenter.php | 1 + app/V1Module/presenters/SubmitPresenter.php | 14 ++++++++--- .../presenters/UploadedFilesPresenter.php | 1 + .../presenters/UserCalendarsPresenter.php | 1 + app/V1Module/presenters/UsersPresenter.php | 5 ++-- .../presenters/WorkerFilesPresenter.php | 1 + .../NetteAnnotationConverter.php | 6 +++-- 38 files changed, 113 insertions(+), 45 deletions(-) diff --git a/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php b/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php index f58d3f41e..922ba6556 100644 --- a/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php +++ b/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/AssignmentSolutionsPresenter.php b/app/V1Module/presenters/AssignmentSolutionsPresenter.php index 7d7bd740a..52ee90eeb 100644 --- a/app/V1Module/presenters/AssignmentSolutionsPresenter.php +++ b/app/V1Module/presenters/AssignmentSolutionsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -344,7 +345,13 @@ public function checkSetBonusPoints(string $id) * @throws InvalidStateException */ #[Post("bonusPoints", new VInt(), "New amount of bonus points, can be negative number")] - #[Post("overriddenPoints", new VString(), "Overrides points assigned to solution by the system", required: false)] + #[Post( + "overriddenPoints", + new VMixed(), + "Overrides points assigned to solution by the system", + required: false, + nullable: true, + )] #[Path("id", new VString(), "Identifier of the solution", required: true)] public function actionSetBonusPoints(string $id) { diff --git a/app/V1Module/presenters/AssignmentSolversPresenter.php b/app/V1Module/presenters/AssignmentSolversPresenter.php index 2ef74fce0..3c97bbc7c 100644 --- a/app/V1Module/presenters/AssignmentSolversPresenter.php +++ b/app/V1Module/presenters/AssignmentSolversPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/AssignmentsPresenter.php b/app/V1Module/presenters/AssignmentsPresenter.php index 6e0db06ff..85b0c0c48 100644 --- a/app/V1Module/presenters/AssignmentsPresenter.php +++ b/app/V1Module/presenters/AssignmentsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -545,8 +546,8 @@ public function actionValidate($id) * @throws InvalidStateException * @throws NotFoundException */ - #[Post("exerciseId", new VString(), "Identifier of the exercise")] - #[Post("groupId", new VString(), "Identifier of the group")] + #[Post("exerciseId", new VMixed(), "Identifier of the exercise", nullable: true)] + #[Post("groupId", new VMixed(), "Identifier of the group", nullable: true)] public function actionCreate() { $req = $this->getRequest(); diff --git a/app/V1Module/presenters/AsyncJobsPresenter.php b/app/V1Module/presenters/AsyncJobsPresenter.php index fb20a2dc3..a84a84f95 100644 --- a/app/V1Module/presenters/AsyncJobsPresenter.php +++ b/app/V1Module/presenters/AsyncJobsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/BrokerPresenter.php b/app/V1Module/presenters/BrokerPresenter.php index ab0737641..63abab256 100644 --- a/app/V1Module/presenters/BrokerPresenter.php +++ b/app/V1Module/presenters/BrokerPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/BrokerReportsPresenter.php b/app/V1Module/presenters/BrokerReportsPresenter.php index 7d17cf425..a3e69aebb 100644 --- a/app/V1Module/presenters/BrokerReportsPresenter.php +++ b/app/V1Module/presenters/BrokerReportsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -183,8 +184,8 @@ private function processJobFailure(JobId $job) * @throws NotFoundException * @throws InvalidStateException */ - #[Post("status", new VString(), "The new status of the job")] - #[Post("message", new VString(), "A textual explanation of the status change", required: false)] + #[Post("status", new VMixed(), "The new status of the job", nullable: true)] + #[Post("message", new VMixed(), "A textual explanation of the status change", required: false, nullable: true)] #[Path("jobId", new VString(), "Identifier of the job whose status is being reported", required: true)] public function actionJobStatus($jobId) { @@ -210,7 +211,7 @@ public function actionJobStatus($jobId) * @POST * @throws InternalServerException */ - #[Post("message", new VString(), "A textual description of the error")] + #[Post("message", new VMixed(), "A textual description of the error", nullable: true)] public function actionError() { $req = $this->getRequest(); diff --git a/app/V1Module/presenters/CommentsPresenter.php b/app/V1Module/presenters/CommentsPresenter.php index b861ed411..bcadf572f 100644 --- a/app/V1Module/presenters/CommentsPresenter.php +++ b/app/V1Module/presenters/CommentsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/DefaultPresenter.php b/app/V1Module/presenters/DefaultPresenter.php index d8be4c176..c48bc6b79 100644 --- a/app/V1Module/presenters/DefaultPresenter.php +++ b/app/V1Module/presenters/DefaultPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/EmailVerificationPresenter.php b/app/V1Module/presenters/EmailVerificationPresenter.php index fad20d0eb..efaea1ecf 100644 --- a/app/V1Module/presenters/EmailVerificationPresenter.php +++ b/app/V1Module/presenters/EmailVerificationPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/EmailsPresenter.php b/app/V1Module/presenters/EmailsPresenter.php index c5925b0d8..72c7bf880 100644 --- a/app/V1Module/presenters/EmailsPresenter.php +++ b/app/V1Module/presenters/EmailsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/ExerciseFilesPresenter.php b/app/V1Module/presenters/ExerciseFilesPresenter.php index 39389360c..e13d1f303 100644 --- a/app/V1Module/presenters/ExerciseFilesPresenter.php +++ b/app/V1Module/presenters/ExerciseFilesPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -101,7 +102,7 @@ public function checkUploadSupplementaryFiles(string $id) * @throws InvalidArgumentException * @throws SubmissionFailedException */ - #[Post("files", new VString(), "Identifiers of supplementary files")] + #[Post("files", new VMixed(), "Identifiers of supplementary files", nullable: true)] #[Path("id", new VString(), "identification of exercise", required: true)] public function actionUploadSupplementaryFiles(string $id) { @@ -262,7 +263,7 @@ public function checkUploadAttachmentFiles(string $id) * @POST * @throws ForbiddenRequestException */ - #[Post("files", new VString(), "Identifiers of attachment files")] + #[Post("files", new VMixed(), "Identifiers of attachment files", nullable: true)] #[Path("id", new VString(), "identification of exercise", required: true)] public function actionUploadAttachmentFiles(string $id) { diff --git a/app/V1Module/presenters/ExercisesConfigPresenter.php b/app/V1Module/presenters/ExercisesConfigPresenter.php index 7b636b5b5..60b89fa5a 100644 --- a/app/V1Module/presenters/ExercisesConfigPresenter.php +++ b/app/V1Module/presenters/ExercisesConfigPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -701,8 +702,9 @@ public function checkSetScoreConfig(string $id) #[Post("scoreCalculator", new VString(), "ID of the score calculator")] #[Post( "scoreConfig", - new VString(), + new VMixed(), "A configuration of the score calculator (the format depends on the calculator type)", + nullable: true, )] #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionSetScoreConfig(string $id) diff --git a/app/V1Module/presenters/ExercisesPresenter.php b/app/V1Module/presenters/ExercisesPresenter.php index 6d81431ff..b72f95cc6 100644 --- a/app/V1Module/presenters/ExercisesPresenter.php +++ b/app/V1Module/presenters/ExercisesPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -333,7 +334,12 @@ public function checkUpdateDetail(string $id) * @throws InvalidArgumentException */ #[Post("version", new VInt(), "Version of the edited exercise")] - #[Post("difficulty", new VString(), "Difficulty of an exercise, should be one of 'easy', 'medium' or 'hard'")] + #[Post( + "difficulty", + new VMixed(), + "Difficulty of an exercise, should be one of 'easy', 'medium' or 'hard'", + nullable: true, + )] #[Post("localizedTexts", new VArray(), "A description of the exercise")] #[Post("isPublic", new VBool(), "Exercise can be public or private", required: false)] #[Post("isLocked", new VBool(), "If true, the exercise cannot be assigned", required: false)] @@ -526,7 +532,7 @@ function (Assignment $assignment) use ($archived) { * @throws ApiException * @throws ParseException */ - #[Post("groupId", new VString(), "Identifier of the group to which exercise belongs to")] + #[Post("groupId", new VMixed(), "Identifier of the group to which exercise belongs to", nullable: true)] public function actionCreate() { $user = $this->getCurrentUser(); @@ -641,7 +647,7 @@ public function actionRemove(string $id) * @throws NotFoundException * @throws ParseException */ - #[Post("groupId", new VString(), "Identifier of the group to which exercise will be forked")] + #[Post("groupId", new VMixed(), "Identifier of the group to which exercise will be forked", nullable: true)] #[Path("id", new VString(), "Identifier of the exercise", required: true)] public function actionForkFrom(string $id) { diff --git a/app/V1Module/presenters/ExtensionsPresenter.php b/app/V1Module/presenters/ExtensionsPresenter.php index 721318a7c..79887e4a8 100644 --- a/app/V1Module/presenters/ExtensionsPresenter.php +++ b/app/V1Module/presenters/ExtensionsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/ForgottenPasswordPresenter.php b/app/V1Module/presenters/ForgottenPasswordPresenter.php index a000f09d1..99f72f317 100644 --- a/app/V1Module/presenters/ForgottenPasswordPresenter.php +++ b/app/V1Module/presenters/ForgottenPasswordPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -110,7 +111,7 @@ public function actionChange() * Check if a password is strong enough * @POST */ - #[Post("password", new VString(), "The password to be checked", nullable: true)] + #[Post("password", new VMixed(), "The password to be checked", nullable: true)] public function actionValidatePasswordStrength() { $password = $this->getRequest()->getPost("password"); diff --git a/app/V1Module/presenters/GroupExternalAttributesPresenter.php b/app/V1Module/presenters/GroupExternalAttributesPresenter.php index 5fac3b588..1401b2e94 100644 --- a/app/V1Module/presenters/GroupExternalAttributesPresenter.php +++ b/app/V1Module/presenters/GroupExternalAttributesPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/GroupInvitationsPresenter.php b/app/V1Module/presenters/GroupInvitationsPresenter.php index 1299667f0..d6d7572ad 100644 --- a/app/V1Module/presenters/GroupInvitationsPresenter.php +++ b/app/V1Module/presenters/GroupInvitationsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -87,7 +88,7 @@ public function checkUpdate($id) * @POST */ #[Post("expireAt", new VTimestamp(), "When the invitation expires.", nullable: true)] - #[Post("note", new VString(), "Note for the students who wish to use the invitation link.")] + #[Post("note", new VMixed(), "Note for the students who wish to use the invitation link.", nullable: true)] #[Path("id", new VString(), required: true)] public function actionUpdate($id) { @@ -181,7 +182,7 @@ public function checkCreate($groupId) * @POST */ #[Post("expireAt", new VTimestamp(), "When the invitation expires.", nullable: true)] - #[Post("note", new VString(), "Note for the students who wish to use the invitation link.")] + #[Post("note", new VMixed(), "Note for the students who wish to use the invitation link.", nullable: true)] #[Path("groupId", new VString(), required: true)] public function actionCreate($groupId) { diff --git a/app/V1Module/presenters/GroupsPresenter.php b/app/V1Module/presenters/GroupsPresenter.php index 818b575a7..bac795c2e 100644 --- a/app/V1Module/presenters/GroupsPresenter.php +++ b/app/V1Module/presenters/GroupsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -255,7 +256,13 @@ private function setGroupPoints(Request $req, Group $group): void * @throws InvalidArgumentException */ #[Post("instanceId", new VUuid(), "An identifier of the instance where the group should be created")] - #[Post("externalId", new VString(), "An informative, human readable identifier of the group", required: false)] + #[Post( + "externalId", + new VMixed(), + "An informative, human readable identifier of the group", + required: false, + nullable: true, + )] #[Post( "parentGroupId", new VUuid(), @@ -338,10 +345,10 @@ public function actionAddGroup() * @POST * @throws ForbiddenRequestException */ - #[Post("name", new VString(), "Name of the group")] - #[Post("locale", new VString(), "The locale of the name")] - #[Post("instanceId", new VString(), "Identifier of the instance where the group belongs")] - #[Post("parentGroupId", new VString(), "Identifier of the parent group", required: false)] + #[Post("name", new VMixed(), "Name of the group", nullable: true)] + #[Post("locale", new VMixed(), "The locale of the name", nullable: true)] + #[Post("instanceId", new VMixed(), "Identifier of the instance where the group belongs", nullable: true)] + #[Post("parentGroupId", new VMixed(), "Identifier of the parent group", required: false, nullable: true)] public function actionValidateAddGroupData() { $req = $this->getRequest(); @@ -375,7 +382,13 @@ public function checkUpdateGroup(string $id) * @POST * @throws InvalidArgumentException */ - #[Post("externalId", new VString(), "An informative, human readable indentifier of the group", required: false)] + #[Post( + "externalId", + new VMixed(), + "An informative, human readable indentifier of the group", + required: false, + nullable: true, + )] #[Post("publicStats", new VBool(), "Should students be able to see each other's results?")] #[Post("detaining", new VBool(), "Are students prevented from leaving the group on their own?", required: false)] #[Post("isPublic", new VBool(), "Should the group be visible to all student?")] diff --git a/app/V1Module/presenters/HardwareGroupsPresenter.php b/app/V1Module/presenters/HardwareGroupsPresenter.php index 10872c00e..252538d5b 100644 --- a/app/V1Module/presenters/HardwareGroupsPresenter.php +++ b/app/V1Module/presenters/HardwareGroupsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/InstancesPresenter.php b/app/V1Module/presenters/InstancesPresenter.php index 84784697e..024687709 100644 --- a/app/V1Module/presenters/InstancesPresenter.php +++ b/app/V1Module/presenters/InstancesPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -121,7 +122,7 @@ public function checkCreateInstance() * @throws ForbiddenRequestException */ #[Post("name", new VString(2), "Name of the instance")] - #[Post("description", new VString(), "Description of the instance", required: false)] + #[Post("description", new VMixed(), "Description of the instance", required: false, nullable: true)] #[Post("isOpen", new VBool(), "Should the instance be open for registration?")] public function actionCreateInstance() { diff --git a/app/V1Module/presenters/LoginPresenter.php b/app/V1Module/presenters/LoginPresenter.php index 421167377..d7aea5713 100644 --- a/app/V1Module/presenters/LoginPresenter.php +++ b/app/V1Module/presenters/LoginPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/NotificationsPresenter.php b/app/V1Module/presenters/NotificationsPresenter.php index 2f088d7ba..61c9376f4 100644 --- a/app/V1Module/presenters/NotificationsPresenter.php +++ b/app/V1Module/presenters/NotificationsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/PipelinesPresenter.php b/app/V1Module/presenters/PipelinesPresenter.php index 4f1399905..a60e4ac5d 100644 --- a/app/V1Module/presenters/PipelinesPresenter.php +++ b/app/V1Module/presenters/PipelinesPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -325,8 +326,8 @@ public function checkUpdatePipeline(string $id) */ #[Post("name", new VString(2), "Name of the pipeline")] #[Post("version", new VInt(), "Version of the edited pipeline")] - #[Post("description", new VString(), "Human readable description of pipeline")] - #[Post("pipeline", new VString(), "Pipeline configuration", required: false)] + #[Post("description", new VMixed(), "Human readable description of pipeline", nullable: true)] + #[Post("pipeline", new VMixed(), "Pipeline configuration", required: false, nullable: true)] #[Post("parameters", new VArray(), "A set of parameters", required: false)] #[Post( "global", @@ -470,7 +471,7 @@ public function checkUploadSupplementaryFiles(string $id) * @throws SubmissionFailedException * @throws NotFoundException */ - #[Post("files", new VString(), "Identifiers of supplementary files")] + #[Post("files", new VMixed(), "Identifiers of supplementary files", nullable: true)] #[Path("id", new VString(), "identification of pipeline", required: true)] public function actionUploadSupplementaryFiles(string $id) { diff --git a/app/V1Module/presenters/PlagiarismPresenter.php b/app/V1Module/presenters/PlagiarismPresenter.php index a928ec1f2..a22693d20 100644 --- a/app/V1Module/presenters/PlagiarismPresenter.php +++ b/app/V1Module/presenters/PlagiarismPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php b/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php index 9dd18ba3a..e8a4f7758 100644 --- a/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php +++ b/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -431,9 +432,9 @@ public function checkSubmit(string $exerciseId) * @throws BadRequestException */ #[Post("note", new VString(), "Description of this particular reference solution, for example used algorithm")] - #[Post("files", new VString(), "Files of the reference solution")] - #[Post("runtimeEnvironmentId", new VString(), "ID of runtime for this solution")] - #[Post("solutionParams", new VString(), "Solution parameters", required: false)] + #[Post("files", new VMixed(), "Files of the reference solution", nullable: true)] + #[Post("runtimeEnvironmentId", new VMixed(), "ID of runtime for this solution", nullable: true)] + #[Post("solutionParams", new VMixed(), "Solution parameters", required: false, nullable: true)] #[Path("exerciseId", new VString(), "Identifier of the exercise", required: true)] public function actionSubmit(string $exerciseId) { diff --git a/app/V1Module/presenters/RegistrationPresenter.php b/app/V1Module/presenters/RegistrationPresenter.php index 4cd07605c..d42f39b01 100644 --- a/app/V1Module/presenters/RegistrationPresenter.php +++ b/app/V1Module/presenters/RegistrationPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -238,8 +239,8 @@ public function actionCreateAccount() * Check if the registered E-mail isn't already used and if the password is strong enough * @POST */ - #[Post("email", new VString(), "E-mail address (login name)")] - #[Post("password", new VString(), "Authentication password", required: false)] + #[Post("email", new VMixed(), "E-mail address (login name)", nullable: true)] + #[Post("password", new VMixed(), "Authentication password", required: false, nullable: true)] public function actionValidateRegistrationData() { $req = $this->getRequest(); diff --git a/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php b/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php index e087c90e2..37247250e 100644 --- a/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php +++ b/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/SecurityPresenter.php b/app/V1Module/presenters/SecurityPresenter.php index 436fc3a3a..ef867893c 100644 --- a/app/V1Module/presenters/SecurityPresenter.php +++ b/app/V1Module/presenters/SecurityPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -38,8 +39,8 @@ class SecurityPresenter extends BasePresenter /** * @POST */ - #[Post("url", new VString(), "URL of the resource that we are checking", required: true)] - #[Post("method", new VString(), "The HTTP method", required: true)] + #[Post("url", new VMixed(), "URL of the resource that we are checking", required: true, nullable: true)] + #[Post("method", new VMixed(), "The HTTP method", required: true, nullable: true)] public function actionCheck() { $requestParams = $this->router->match( diff --git a/app/V1Module/presenters/ShadowAssignmentsPresenter.php b/app/V1Module/presenters/ShadowAssignmentsPresenter.php index 72cdf24c1..095f04737 100644 --- a/app/V1Module/presenters/ShadowAssignmentsPresenter.php +++ b/app/V1Module/presenters/ShadowAssignmentsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -261,7 +262,7 @@ public function actionUpdateDetail(string $id) * @throws BadRequestException * @throws NotFoundException */ - #[Post("groupId", new VString(), "Identifier of the group")] + #[Post("groupId", new VMixed(), "Identifier of the group", nullable: true)] public function actionCreate() { $req = $this->getRequest(); diff --git a/app/V1Module/presenters/SisPresenter.php b/app/V1Module/presenters/SisPresenter.php index 53aeaa127..2fc8c54b1 100644 --- a/app/V1Module/presenters/SisPresenter.php +++ b/app/V1Module/presenters/SisPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -150,8 +151,8 @@ public function checkRegisterTerm() * @throws ForbiddenRequestException * @throws BadRequestException */ - #[Post("year", new VString())] - #[Post("term", new VString())] + #[Post("year", new VMixed(), nullable: true)] + #[Post("term", new VMixed(), nullable: true)] public function actionRegisterTerm() { $year = intval($this->getRequest()->getPost("year")); @@ -422,7 +423,7 @@ private function makeCaptionsUnique(array &$captions, Group $parentGroup) * @throws InvalidArgumentException * @throws Exception */ - #[Post("parentGroupId", new VString())] + #[Post("parentGroupId", new VMixed(), nullable: true)] #[Path("courseId", new VString(), required: true)] public function actionCreateGroup($courseId) { @@ -495,7 +496,7 @@ public function actionCreateGroup($courseId) * @throws ForbiddenRequestException * @throws BadRequestException */ - #[Post("groupId", new VString())] + #[Post("groupId", new VMixed(), nullable: true)] #[Path("courseId", new VString(), required: true)] public function actionBindGroup($courseId) { diff --git a/app/V1Module/presenters/SubmissionFailuresPresenter.php b/app/V1Module/presenters/SubmissionFailuresPresenter.php index 1cf34aa65..94d066c0a 100644 --- a/app/V1Module/presenters/SubmissionFailuresPresenter.php +++ b/app/V1Module/presenters/SubmissionFailuresPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/SubmitPresenter.php b/app/V1Module/presenters/SubmitPresenter.php index 1709f21a0..221bd55a9 100644 --- a/app/V1Module/presenters/SubmitPresenter.php +++ b/app/V1Module/presenters/SubmitPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -249,10 +250,15 @@ public function actionCanSubmit(string $id, string $userId = null) * @throws ParseException */ #[Post("note", new VString(0, 1024), "A note by the author of the solution")] - #[Post("userId", new VString(), "Author of the submission", required: false)] - #[Post("files", new VString(), "Submitted files")] - #[Post("runtimeEnvironmentId", new VString(), "Identifier of the runtime environment used for evaluation")] - #[Post("solutionParams", new VString(), "Solution parameters", required: false)] + #[Post("userId", new VMixed(), "Author of the submission", required: false, nullable: true)] + #[Post("files", new VMixed(), "Submitted files", nullable: true)] + #[Post( + "runtimeEnvironmentId", + new VMixed(), + "Identifier of the runtime environment used for evaluation", + nullable: true, + )] + #[Post("solutionParams", new VMixed(), "Solution parameters", required: false, nullable: true)] #[Path("id", new VString(), "Identifier of the assignment", required: true)] public function actionSubmit(string $id) { diff --git a/app/V1Module/presenters/UploadedFilesPresenter.php b/app/V1Module/presenters/UploadedFilesPresenter.php index 639df56b4..32636672b 100644 --- a/app/V1Module/presenters/UploadedFilesPresenter.php +++ b/app/V1Module/presenters/UploadedFilesPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/UserCalendarsPresenter.php b/app/V1Module/presenters/UserCalendarsPresenter.php index adaa2be91..4f3264e68 100644 --- a/app/V1Module/presenters/UserCalendarsPresenter.php +++ b/app/V1Module/presenters/UserCalendarsPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/V1Module/presenters/UsersPresenter.php b/app/V1Module/presenters/UsersPresenter.php index a0bf06ca8..4c47ac83d 100644 --- a/app/V1Module/presenters/UsersPresenter.php +++ b/app/V1Module/presenters/UsersPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; @@ -248,8 +249,8 @@ public function checkUpdateProfile(string $id) */ #[Post("firstName", new VString(2), "First name", required: false)] #[Post("lastName", new VString(2), "Last name", required: false)] - #[Post("titlesBeforeName", new VString(), "Titles before name", required: false)] - #[Post("titlesAfterName", new VString(), "Titles after name", required: false)] + #[Post("titlesBeforeName", new VMixed(), "Titles before name", required: false, nullable: true)] + #[Post("titlesAfterName", new VMixed(), "Titles after name", required: false, nullable: true)] #[Post("email", new VEmail(), "New email address", required: false)] #[Post("oldPassword", new VString(1), "Old password of current user", required: false)] #[Post("password", new VString(1), "New password of current user", required: false)] diff --git a/app/V1Module/presenters/WorkerFilesPresenter.php b/app/V1Module/presenters/WorkerFilesPresenter.php index 65f78762c..6537d3c1b 100644 --- a/app/V1Module/presenters/WorkerFilesPresenter.php +++ b/app/V1Module/presenters/WorkerFilesPresenter.php @@ -11,6 +11,7 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; +use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\MetaFormats\Validators\VTimestamp; use App\Helpers\MetaFormats\Validators\VUuid; diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index c02a95132..6ba05a23d 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -225,11 +225,13 @@ public static function convertRegexCapturesToParenthesesBuilder(array $annotatio } $parenthesesBuilder->addValue("\"{$annotationParameters["name"]}\""); + $nullable = false; // replace missing validations with placeholder validations if (!array_key_exists("validation", $annotationParameters)) { $annotationParameters["validation"] = "mixed"; + // missing validations imply nullability + $nullable = true; } - $nullable = false; $validation = $annotationParameters["validation"]; if (Utils::checkValidationNullability($validation)) { @@ -246,7 +248,7 @@ public static function convertRegexCapturesToParenthesesBuilder(array $annotatio // escape all quotes and dollar signs $description = str_replace("\"", "\\\"", $description); $description = str_replace("$", "\\$", $description); - $parenthesesBuilder->addValue("\"{$description}\""); + $parenthesesBuilder->addValue(value: "\"{$description}\""); } if (array_key_exists("required", $annotationParameters)) { From da2bf9765f7a6f980570801b4a97f380a9974c20 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 18:50:39 +0100 Subject: [PATCH 050/103] refactored the conversion command --- .../{MetaTester.php => MetaConverter.php} | 52 ++++--------------- 1 file changed, 10 insertions(+), 42 deletions(-) rename app/commands/{MetaTester.php => MetaConverter.php} (52%) diff --git a/app/commands/MetaTester.php b/app/commands/MetaConverter.php similarity index 52% rename from app/commands/MetaTester.php rename to app/commands/MetaConverter.php index 58681e58e..3f16326cc 100644 --- a/app/commands/MetaTester.php +++ b/app/commands/MetaConverter.php @@ -3,33 +3,29 @@ namespace App\Console; use App\Helpers\MetaFormats\AnnotationConversion\AnnotationToAttributeConverter; -use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute; -use App\Helpers\MetaFormats\FormatDefinitions\GroupFormat; -use App\Helpers\MetaFormats\FormatDefinitions\UserFormat; -use App\Helpers\MetaFormats\MetaFormatHelper; -use App\Helpers\MetaFormats\Validators\VArray; -use App\Helpers\MetaFormats\Validators\VString; -use App\Helpers\Swagger\AnnotationHelper; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -///TODO: this command is debug only, delete it -class MetaTester extends Command +/** + * Scans all Presenters in V1Module and creates a copy of the containing 'presenters' folder, in which all endpoint + * parameter annotations are converted into attributes. + * The new folder is named 'presenters2'. + */ +class MetaConverter extends Command { - protected static $defaultName = 'meta:test'; + protected static $defaultName = 'meta:convert'; protected function configure() { $this->setName(self::$defaultName)->setDescription( - 'Test the meta views.' + 'Convert endpoint parameter annotations to attributes..' ); } protected function execute(InputInterface $input, OutputInterface $output) { - $this->test("a"); + $this->generatePresenters(); return Command::SUCCESS; } @@ -56,6 +52,7 @@ public function generatePresenters() } } + // copy and convert Presenters $filenames = scandir($inDir); foreach ($filenames as $filename) { if (!str_ends_with($filename, "Presenter.php")) { @@ -69,33 +66,4 @@ public function generatePresenters() fclose($newFile); } } - - public function test(string $arg) - { - // $view = new TestView(); - // $view->endpoint([ - // "id" => "0", - // "organizational" => false, - // ], "0001"); - // // $view->get_user_info(0); - - // $format = new GroupFormat(); - // var_dump($format->checkIfAssignable("primaryAdminsIds", [ "10000000-2000-4000-8000-160000000000", "10000000-2000-4000-8000-160000000000" ])); - - // $format = new UserFormat(); - // var_dump($format->checkedAssign("email", "a@a.a.a")); - - - // $reflection = AnnotationHelper::getMethod("App\V1Module\Presenters\RegistrationPresenter", "actionCreateAccount"); - // $attrs = MetaFormatHelper::extractRequestParamData($reflection); - // var_dump($attrs); - - $this->generatePresenters(); - - // $val = new VArray(); - - // $name = get_class($val) . "::DEFAULT_SWAGGER_VALUE"; - // var_dump($name); - // var_dump(defined($name)); - } } From 38ff805aea6da239e1cd58240567c25633bdf655 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 19:14:57 +0100 Subject: [PATCH 051/103] bugfix: changed command name in config --- app/config/config.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/config.neon b/app/config/config.neon index ed55f8e09..3c8b17da3 100644 --- a/app/config/config.neon +++ b/app/config/config.neon @@ -320,7 +320,7 @@ services: - App\Console\AsyncJobsUpkeep(%async.upkeep%) - App\Console\GeneralStatsNotification - App\Console\ExportDatabase - - App\Console\MetaTester + - App\Console\MetaConverter - App\Console\GenerateSwagger - App\Console\SwaggerAnnotator - App\Console\CleanupLocalizedTexts From 57f9d73316cd02163b1cbd0a8960bba587075973 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 19:25:37 +0100 Subject: [PATCH 052/103] implementing validators WIP --- app/helpers/MetaFormats/Validators/VArray.php | 6 ++++-- app/helpers/MetaFormats/Validators/VInt.php | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VArray.php b/app/helpers/MetaFormats/Validators/VArray.php index 111446289..e457d1dfe 100644 --- a/app/helpers/MetaFormats/Validators/VArray.php +++ b/app/helpers/MetaFormats/Validators/VArray.php @@ -26,9 +26,11 @@ public function getExampleValue() return null; } - public function getElementSwaggerType() + /** + * @return string|null Returns the element swagger type. Can be null if the element validator is not set. + */ + public function getElementSwaggerType(): mixed { - // default to string for unknown element types if ($this->nestedValidator === null) { return null; } diff --git a/app/helpers/MetaFormats/Validators/VInt.php b/app/helpers/MetaFormats/Validators/VInt.php index 953212e86..bf5315690 100644 --- a/app/helpers/MetaFormats/Validators/VInt.php +++ b/app/helpers/MetaFormats/Validators/VInt.php @@ -2,6 +2,7 @@ namespace App\Helpers\MetaFormats\Validators; +use App\Exceptions\InternalServerException; use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; @@ -11,6 +12,7 @@ class VInt public function validate(mixed $value) { + // throw new InternalServerException("integer:" . gettype($value)); ///TODO: check if int return true; } From 19725866f30cdfa40dc3c91f3ae04ebcbb45c302 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 20:06:59 +0100 Subject: [PATCH 053/103] implemented int validator --- app/helpers/MetaFormats/Validators/VInt.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VInt.php b/app/helpers/MetaFormats/Validators/VInt.php index bf5315690..f007529ca 100644 --- a/app/helpers/MetaFormats/Validators/VInt.php +++ b/app/helpers/MetaFormats/Validators/VInt.php @@ -3,6 +3,7 @@ namespace App\Helpers\MetaFormats\Validators; use App\Exceptions\InternalServerException; +use App\Helpers\MetaFormats\MetaFormatHelper; use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; @@ -10,10 +11,13 @@ class VInt { public const SWAGGER_TYPE = "integer"; + public function getExampleValue() + { + return "0"; + } + public function validate(mixed $value) { - // throw new InternalServerException("integer:" . gettype($value)); - ///TODO: check if int - return true; + return MetaFormatHelper::checkType($value, PhpTypes::Int); } } From 6e701c86f88dfedb16dcd296e9f6e7d7522ba0a5 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 20:15:39 +0100 Subject: [PATCH 054/103] renamed VFloat to VDouble --- .../presenters/AssignmentSolutionReviewsPresenter.php | 2 +- app/V1Module/presenters/AssignmentSolutionsPresenter.php | 2 +- app/V1Module/presenters/AssignmentSolversPresenter.php | 2 +- app/V1Module/presenters/AssignmentsPresenter.php | 4 ++-- app/V1Module/presenters/AsyncJobsPresenter.php | 2 +- app/V1Module/presenters/BrokerPresenter.php | 2 +- app/V1Module/presenters/BrokerReportsPresenter.php | 2 +- app/V1Module/presenters/CommentsPresenter.php | 2 +- app/V1Module/presenters/DefaultPresenter.php | 2 +- app/V1Module/presenters/EmailVerificationPresenter.php | 2 +- app/V1Module/presenters/EmailsPresenter.php | 2 +- app/V1Module/presenters/ExerciseFilesPresenter.php | 2 +- app/V1Module/presenters/ExercisesConfigPresenter.php | 2 +- app/V1Module/presenters/ExercisesPresenter.php | 2 +- app/V1Module/presenters/ExtensionsPresenter.php | 2 +- app/V1Module/presenters/ForgottenPasswordPresenter.php | 2 +- app/V1Module/presenters/GroupExternalAttributesPresenter.php | 2 +- app/V1Module/presenters/GroupInvitationsPresenter.php | 2 +- app/V1Module/presenters/GroupsPresenter.php | 2 +- app/V1Module/presenters/HardwareGroupsPresenter.php | 2 +- app/V1Module/presenters/InstancesPresenter.php | 2 +- app/V1Module/presenters/LoginPresenter.php | 2 +- app/V1Module/presenters/NotificationsPresenter.php | 2 +- app/V1Module/presenters/PipelinesPresenter.php | 2 +- app/V1Module/presenters/PlagiarismPresenter.php | 4 ++-- .../presenters/ReferenceExerciseSolutionsPresenter.php | 2 +- app/V1Module/presenters/RegistrationPresenter.php | 2 +- app/V1Module/presenters/RuntimeEnvironmentsPresenter.php | 2 +- app/V1Module/presenters/SecurityPresenter.php | 2 +- app/V1Module/presenters/ShadowAssignmentsPresenter.php | 2 +- app/V1Module/presenters/SisPresenter.php | 2 +- app/V1Module/presenters/SubmissionFailuresPresenter.php | 2 +- app/V1Module/presenters/SubmitPresenter.php | 2 +- app/V1Module/presenters/UploadedFilesPresenter.php | 2 +- app/V1Module/presenters/UserCalendarsPresenter.php | 2 +- app/V1Module/presenters/UsersPresenter.php | 2 +- app/V1Module/presenters/WorkerFilesPresenter.php | 2 +- .../AnnotationConversion/NetteAnnotationConverter.php | 4 ++-- .../MetaFormats/Validators/{VFloat.php => VDouble.php} | 2 +- 39 files changed, 42 insertions(+), 42 deletions(-) rename app/helpers/MetaFormats/Validators/{VFloat.php => VDouble.php} (95%) diff --git a/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php b/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php index 922ba6556..181a6634e 100644 --- a/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php +++ b/app/V1Module/presenters/AssignmentSolutionReviewsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/AssignmentSolutionsPresenter.php b/app/V1Module/presenters/AssignmentSolutionsPresenter.php index 52ee90eeb..f7fd82e61 100644 --- a/app/V1Module/presenters/AssignmentSolutionsPresenter.php +++ b/app/V1Module/presenters/AssignmentSolutionsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/AssignmentSolversPresenter.php b/app/V1Module/presenters/AssignmentSolversPresenter.php index 3c97bbc7c..48385c815 100644 --- a/app/V1Module/presenters/AssignmentSolversPresenter.php +++ b/app/V1Module/presenters/AssignmentSolversPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/AssignmentsPresenter.php b/app/V1Module/presenters/AssignmentsPresenter.php index 85b0c0c48..287b9931a 100644 --- a/app/V1Module/presenters/AssignmentsPresenter.php +++ b/app/V1Module/presenters/AssignmentsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; @@ -268,7 +268,7 @@ public function checkUpdateDetail(string $id) #[Post("isBonus", new VBool(), "If true, points from this exercise will not be included in overall score of group")] #[Post( "pointsPercentualThreshold", - new VFloat(), + new VDouble(), "A minimum percentage of points needed to gain point from assignment", required: false, )] diff --git a/app/V1Module/presenters/AsyncJobsPresenter.php b/app/V1Module/presenters/AsyncJobsPresenter.php index a84a84f95..1aaea3534 100644 --- a/app/V1Module/presenters/AsyncJobsPresenter.php +++ b/app/V1Module/presenters/AsyncJobsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/BrokerPresenter.php b/app/V1Module/presenters/BrokerPresenter.php index 63abab256..a28f16087 100644 --- a/app/V1Module/presenters/BrokerPresenter.php +++ b/app/V1Module/presenters/BrokerPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/BrokerReportsPresenter.php b/app/V1Module/presenters/BrokerReportsPresenter.php index a3e69aebb..8978d9e33 100644 --- a/app/V1Module/presenters/BrokerReportsPresenter.php +++ b/app/V1Module/presenters/BrokerReportsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/CommentsPresenter.php b/app/V1Module/presenters/CommentsPresenter.php index bcadf572f..5f4396b24 100644 --- a/app/V1Module/presenters/CommentsPresenter.php +++ b/app/V1Module/presenters/CommentsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/DefaultPresenter.php b/app/V1Module/presenters/DefaultPresenter.php index c48bc6b79..2e4525cc7 100644 --- a/app/V1Module/presenters/DefaultPresenter.php +++ b/app/V1Module/presenters/DefaultPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/EmailVerificationPresenter.php b/app/V1Module/presenters/EmailVerificationPresenter.php index efaea1ecf..2c8946d4a 100644 --- a/app/V1Module/presenters/EmailVerificationPresenter.php +++ b/app/V1Module/presenters/EmailVerificationPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/EmailsPresenter.php b/app/V1Module/presenters/EmailsPresenter.php index 72c7bf880..2e683f5fb 100644 --- a/app/V1Module/presenters/EmailsPresenter.php +++ b/app/V1Module/presenters/EmailsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/ExerciseFilesPresenter.php b/app/V1Module/presenters/ExerciseFilesPresenter.php index e13d1f303..538f3a5cf 100644 --- a/app/V1Module/presenters/ExerciseFilesPresenter.php +++ b/app/V1Module/presenters/ExerciseFilesPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/ExercisesConfigPresenter.php b/app/V1Module/presenters/ExercisesConfigPresenter.php index 60b89fa5a..90cda8602 100644 --- a/app/V1Module/presenters/ExercisesConfigPresenter.php +++ b/app/V1Module/presenters/ExercisesConfigPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/ExercisesPresenter.php b/app/V1Module/presenters/ExercisesPresenter.php index b72f95cc6..46a3d286c 100644 --- a/app/V1Module/presenters/ExercisesPresenter.php +++ b/app/V1Module/presenters/ExercisesPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/ExtensionsPresenter.php b/app/V1Module/presenters/ExtensionsPresenter.php index 79887e4a8..727f7caa8 100644 --- a/app/V1Module/presenters/ExtensionsPresenter.php +++ b/app/V1Module/presenters/ExtensionsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/ForgottenPasswordPresenter.php b/app/V1Module/presenters/ForgottenPasswordPresenter.php index 99f72f317..51a8fbd8c 100644 --- a/app/V1Module/presenters/ForgottenPasswordPresenter.php +++ b/app/V1Module/presenters/ForgottenPasswordPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/GroupExternalAttributesPresenter.php b/app/V1Module/presenters/GroupExternalAttributesPresenter.php index 1401b2e94..809106c36 100644 --- a/app/V1Module/presenters/GroupExternalAttributesPresenter.php +++ b/app/V1Module/presenters/GroupExternalAttributesPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/GroupInvitationsPresenter.php b/app/V1Module/presenters/GroupInvitationsPresenter.php index d6d7572ad..d537233ce 100644 --- a/app/V1Module/presenters/GroupInvitationsPresenter.php +++ b/app/V1Module/presenters/GroupInvitationsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/GroupsPresenter.php b/app/V1Module/presenters/GroupsPresenter.php index bac795c2e..1f97226f7 100644 --- a/app/V1Module/presenters/GroupsPresenter.php +++ b/app/V1Module/presenters/GroupsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/HardwareGroupsPresenter.php b/app/V1Module/presenters/HardwareGroupsPresenter.php index 252538d5b..a930499b3 100644 --- a/app/V1Module/presenters/HardwareGroupsPresenter.php +++ b/app/V1Module/presenters/HardwareGroupsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/InstancesPresenter.php b/app/V1Module/presenters/InstancesPresenter.php index 024687709..17cc60b8d 100644 --- a/app/V1Module/presenters/InstancesPresenter.php +++ b/app/V1Module/presenters/InstancesPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/LoginPresenter.php b/app/V1Module/presenters/LoginPresenter.php index d7aea5713..47e3c526a 100644 --- a/app/V1Module/presenters/LoginPresenter.php +++ b/app/V1Module/presenters/LoginPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/NotificationsPresenter.php b/app/V1Module/presenters/NotificationsPresenter.php index 61c9376f4..3f28aae06 100644 --- a/app/V1Module/presenters/NotificationsPresenter.php +++ b/app/V1Module/presenters/NotificationsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/PipelinesPresenter.php b/app/V1Module/presenters/PipelinesPresenter.php index a60e4ac5d..2a6003a3d 100644 --- a/app/V1Module/presenters/PipelinesPresenter.php +++ b/app/V1Module/presenters/PipelinesPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/PlagiarismPresenter.php b/app/V1Module/presenters/PlagiarismPresenter.php index a22693d20..d8959c430 100644 --- a/app/V1Module/presenters/PlagiarismPresenter.php +++ b/app/V1Module/presenters/PlagiarismPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; @@ -235,7 +235,7 @@ public function checkAddSimilarities(string $id, string $solutionId): void required: false, )] #[Post("authorId", new VUuid(), "Id of the author of the similar solutions/files.")] - #[Post("similarity", new VFloat(), "Relative similarity of the records associated with selected author [0-1].")] + #[Post("similarity", new VDouble(), "Relative similarity of the records associated with selected author [0-1].")] #[Post("files", new VArray(), "List of similar files and their records.")] #[Path("id", new VString(), required: true)] #[Path("solutionId", new VString(), required: true)] diff --git a/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php b/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php index e8a4f7758..bf31298b3 100644 --- a/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php +++ b/app/V1Module/presenters/ReferenceExerciseSolutionsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/RegistrationPresenter.php b/app/V1Module/presenters/RegistrationPresenter.php index d42f39b01..6fc89d3b6 100644 --- a/app/V1Module/presenters/RegistrationPresenter.php +++ b/app/V1Module/presenters/RegistrationPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php b/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php index 37247250e..5799d6761 100644 --- a/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php +++ b/app/V1Module/presenters/RuntimeEnvironmentsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/SecurityPresenter.php b/app/V1Module/presenters/SecurityPresenter.php index ef867893c..7ff6a1aee 100644 --- a/app/V1Module/presenters/SecurityPresenter.php +++ b/app/V1Module/presenters/SecurityPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/ShadowAssignmentsPresenter.php b/app/V1Module/presenters/ShadowAssignmentsPresenter.php index 095f04737..51a356ac4 100644 --- a/app/V1Module/presenters/ShadowAssignmentsPresenter.php +++ b/app/V1Module/presenters/ShadowAssignmentsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/SisPresenter.php b/app/V1Module/presenters/SisPresenter.php index 2fc8c54b1..7a0a019d2 100644 --- a/app/V1Module/presenters/SisPresenter.php +++ b/app/V1Module/presenters/SisPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/SubmissionFailuresPresenter.php b/app/V1Module/presenters/SubmissionFailuresPresenter.php index 94d066c0a..96b33e2e5 100644 --- a/app/V1Module/presenters/SubmissionFailuresPresenter.php +++ b/app/V1Module/presenters/SubmissionFailuresPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/SubmitPresenter.php b/app/V1Module/presenters/SubmitPresenter.php index 221bd55a9..20190befd 100644 --- a/app/V1Module/presenters/SubmitPresenter.php +++ b/app/V1Module/presenters/SubmitPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/UploadedFilesPresenter.php b/app/V1Module/presenters/UploadedFilesPresenter.php index 32636672b..77a986d00 100644 --- a/app/V1Module/presenters/UploadedFilesPresenter.php +++ b/app/V1Module/presenters/UploadedFilesPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/UserCalendarsPresenter.php b/app/V1Module/presenters/UserCalendarsPresenter.php index 4f3264e68..7f6090115 100644 --- a/app/V1Module/presenters/UserCalendarsPresenter.php +++ b/app/V1Module/presenters/UserCalendarsPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/UsersPresenter.php b/app/V1Module/presenters/UsersPresenter.php index 4c47ac83d..129296b27 100644 --- a/app/V1Module/presenters/UsersPresenter.php +++ b/app/V1Module/presenters/UsersPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/V1Module/presenters/WorkerFilesPresenter.php b/app/V1Module/presenters/WorkerFilesPresenter.php index 6537d3c1b..c9a7b5b6e 100644 --- a/app/V1Module/presenters/WorkerFilesPresenter.php +++ b/app/V1Module/presenters/WorkerFilesPresenter.php @@ -8,8 +8,8 @@ use App\Helpers\MetaFormats\Type; use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index 6ba05a23d..97b422c15 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -6,7 +6,7 @@ use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VBool; use App\Helpers\MetaFormats\Validators\VEmail; -use App\Helpers\MetaFormats\Validators\VFloat; +use App\Helpers\MetaFormats\Validators\VDouble; use App\Helpers\MetaFormats\Validators\VInt; use App\Helpers\MetaFormats\Validators\VMixed; use App\Helpers\MetaFormats\Validators\VString; @@ -195,7 +195,7 @@ private static function convertAnnotationValidationToValidatorString(string $val $validatorClass = VTimestamp::class; break; case "numeric": - $validatorClass = VFloat::class; + $validatorClass = VDouble::class; break; case "mixed": $validatorClass = VMixed::class; diff --git a/app/helpers/MetaFormats/Validators/VFloat.php b/app/helpers/MetaFormats/Validators/VDouble.php similarity index 95% rename from app/helpers/MetaFormats/Validators/VFloat.php rename to app/helpers/MetaFormats/Validators/VDouble.php index 148635ffd..05f547d59 100644 --- a/app/helpers/MetaFormats/Validators/VFloat.php +++ b/app/helpers/MetaFormats/Validators/VDouble.php @@ -5,7 +5,7 @@ use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; -class VFloat +class VDouble { public const SWAGGER_TYPE = "number"; From 0ad910e4920e90bcd50557a518d42ae47c972a10 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 20:31:18 +0100 Subject: [PATCH 055/103] made parameter error messages more verbose --- app/helpers/MetaFormats/RequestParamData.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 50db5fe89..d5fdb0fec 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -65,7 +65,11 @@ public function conformsToDefinition(mixed $value) // use every provided validator foreach ($this->validators as $validator) { if (!$validator->validate($value)) { - throw new InvalidArgumentException($this->name); + $type = $validator::SWAGGER_TYPE; + throw new InvalidArgumentException( + $this->name, + "The provided value for parameter '{$this->name}' did not pass validation of type '{$type}'." + ); } } From 14dbbffa32ce2ff78bf5a2ab54719d770bc10f3f Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 20:37:23 +0100 Subject: [PATCH 056/103] changed error message --- app/helpers/MetaFormats/RequestParamData.php | 2 +- app/helpers/MetaFormats/Validators/VInt.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index d5fdb0fec..6db452e51 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -68,7 +68,7 @@ public function conformsToDefinition(mixed $value) $type = $validator::SWAGGER_TYPE; throw new InvalidArgumentException( $this->name, - "The provided value for parameter '{$this->name}' did not pass validation of type '{$type}'." + "The provided value did not pass the validation of type '{$type}'." ); } } diff --git a/app/helpers/MetaFormats/Validators/VInt.php b/app/helpers/MetaFormats/Validators/VInt.php index f007529ca..bc2c31e81 100644 --- a/app/helpers/MetaFormats/Validators/VInt.php +++ b/app/helpers/MetaFormats/Validators/VInt.php @@ -18,6 +18,9 @@ public function getExampleValue() public function validate(mixed $value) { + if (!MetaFormatHelper::checkType($value, PhpTypes::Int)) { + throw new InternalServerException("err: {$value}"); + } return MetaFormatHelper::checkType($value, PhpTypes::Int); } } From cd8c805a19c77d75bd8b09cdb100b63fd5c8e843 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 21:01:10 +0100 Subject: [PATCH 057/103] int validator now supports numeric strings --- app/helpers/MetaFormats/Validators/VInt.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VInt.php b/app/helpers/MetaFormats/Validators/VInt.php index bc2c31e81..5ef10b2e8 100644 --- a/app/helpers/MetaFormats/Validators/VInt.php +++ b/app/helpers/MetaFormats/Validators/VInt.php @@ -18,9 +18,16 @@ public function getExampleValue() public function validate(mixed $value) { - if (!MetaFormatHelper::checkType($value, PhpTypes::Int)) { - throw new InternalServerException("err: {$value}"); + // check if it is an integer + if (MetaFormatHelper::checkType($value, PhpTypes::Int)) { + return true; } - return MetaFormatHelper::checkType($value, PhpTypes::Int); + + // the value may be a string containing the integer + if (!is_numeric($value)) { + return false; + } + + return intval($value) == floatval($value); } } From c400a1f8761225eacb6619256059138a7c2d9b5f Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 20 Feb 2025 21:13:07 +0100 Subject: [PATCH 058/103] implemented more validators --- app/helpers/MetaFormats/PhpTypes.php | 1 + app/helpers/MetaFormats/Validators/VArray.php | 18 +++++++++++++++++- app/helpers/MetaFormats/Validators/VBool.php | 5 +++-- app/helpers/MetaFormats/Validators/VDouble.php | 10 ++++++++-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/helpers/MetaFormats/PhpTypes.php b/app/helpers/MetaFormats/PhpTypes.php index 2a60fc449..22b1105f4 100644 --- a/app/helpers/MetaFormats/PhpTypes.php +++ b/app/helpers/MetaFormats/PhpTypes.php @@ -11,5 +11,6 @@ enum PhpTypes: string case Double = "double"; case Object = "object"; case Null = "NULL"; + case Bool = "boolean"; } // @codingStandardsIgnoreEnd diff --git a/app/helpers/MetaFormats/Validators/VArray.php b/app/helpers/MetaFormats/Validators/VArray.php index e457d1dfe..aa72f916d 100644 --- a/app/helpers/MetaFormats/Validators/VArray.php +++ b/app/helpers/MetaFormats/Validators/VArray.php @@ -12,6 +12,11 @@ class VArray // validator used for elements private mixed $nestedValidator; + /** + * Creates an array validator. + * @param mixed $nestedValidator A validator that will be applied on all elements + * (validator arrays are not supported). + */ public function __construct(mixed $nestedValidator = null) { $this->nestedValidator = $nestedValidator; @@ -40,7 +45,18 @@ public function getElementSwaggerType(): mixed public function validate(mixed $value) { - ///TODO: check if array, check content + if (!is_array($value)) { + return false; + } + + // validate all elements if there is a nested validator + if ($this->nestedValidator != null) { + foreach ($value as $element) { + if (!$this->nestedValidator->validate($element)) { + return false; + } + } + } return true; } } diff --git a/app/helpers/MetaFormats/Validators/VBool.php b/app/helpers/MetaFormats/Validators/VBool.php index 68c58c27f..b45747d47 100644 --- a/app/helpers/MetaFormats/Validators/VBool.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -2,6 +2,7 @@ namespace App\Helpers\MetaFormats\Validators; +use App\Helpers\MetaFormats\MetaFormatHelper; use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; @@ -11,7 +12,7 @@ class VBool public function validate(mixed $value) { - ///TODO: check if bool - return true; + // support stringified values as well + return MetaFormatHelper::checkType($value, PhpTypes::Bool) || $value == "true" || $value == "false"; } } diff --git a/app/helpers/MetaFormats/Validators/VDouble.php b/app/helpers/MetaFormats/Validators/VDouble.php index 05f547d59..97e404ffc 100644 --- a/app/helpers/MetaFormats/Validators/VDouble.php +++ b/app/helpers/MetaFormats/Validators/VDouble.php @@ -2,6 +2,7 @@ namespace App\Helpers\MetaFormats\Validators; +use App\Helpers\MetaFormats\MetaFormatHelper; use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; @@ -11,7 +12,12 @@ class VDouble public function validate(mixed $value) { - ///TODO: check if float - return true; + // check if it is a double + if (MetaFormatHelper::checkType($value, PhpTypes::Double)) { + return true; + } + + // the value may be a string containing the number + return is_numeric($value); } } From ffee4a5ebe408a852c1b454625abff9d0c53ff93 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 11:15:57 +0100 Subject: [PATCH 059/103] resolved some todos --- app/helpers/MetaFormats/MetaFormatHelper.php | 1 - app/helpers/MetaFormats/MetaRequest.php | 1 - app/helpers/MetaFormats/RequestParamData.php | 8 ++++---- app/helpers/Swagger/AnnotationHelper.php | 5 +++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index 65bef3dd7..32190d290 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -154,7 +154,6 @@ public static function createNameToFieldDefinitionsMap(string $className): array ); } - ///TODO: add base type (PHP type of the field) validators to $requestParamData $formats[$fieldName] = $requestParamData; } diff --git a/app/helpers/MetaFormats/MetaRequest.php b/app/helpers/MetaFormats/MetaRequest.php index e1cf56874..fba9ccc28 100644 --- a/app/helpers/MetaFormats/MetaRequest.php +++ b/app/helpers/MetaFormats/MetaRequest.php @@ -43,7 +43,6 @@ public function getParameter(string $key): mixed * Returns a variable provided to the presenter via POST. * If no key is passed, returns the entire array. */ - ///TODO: how should null be handled? public function getPost(?string $key = null): mixed { if ($key === null) { diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 6db452e51..fac2e8bbe 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -44,12 +44,12 @@ public function conformsToDefinition(mixed $value) { // check if null if ($value === null) { + // optional parameters can be null if (!$this->required) { - ///TODO: what if a required param can be null? Does that mean that required & null is fine? How to check the required constrains then? - //throw new InvalidArgumentException($this->name, "The parameter is required and cannot be null."); return true; } + // required parameters can be null only if explicitly nullable if (!$this->nullable) { throw new InvalidArgumentException( $this->name, @@ -92,9 +92,10 @@ public function toAnnotationParameterData() ); } - $swaggerType = "string"; + // determine swagger type $nestedArraySwaggerType = null; $swaggerType = $this->validators[0]::SWAGGER_TYPE; + // extract array element type if ($this->validators[0] instanceof VArray) { $nestedArraySwaggerType = $this->validators[0]->getElementSwaggerType(); } @@ -105,7 +106,6 @@ public function toAnnotationParameterData() $exampleValue = $this->validators[0]->getExampleValue(); } - ///TODO: does not pass null return new AnnotationParameterData( $swaggerType, $this->name, diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index 9251126ba..589fb0399 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -2,6 +2,7 @@ namespace App\Helpers\Swagger; +use App\Exceptions\InvalidArgumentException; use App\Helpers\MetaFormats\MetaFormatHelper; use App\V1Module\Router\MethodRoute; use App\V1Module\RouterFactory; @@ -113,7 +114,7 @@ private static function getSwaggerType(string $annotationType): string } if (self::$typeMap[$typename] === null) { - throw new \InvalidArgumentException("Error in getSwaggerType: Unknown typename: {$typename}"); + throw new InvalidArgumentException("Error in getSwaggerType: Unknown typename: {$typename}"); } $type = self::$typeMap[$typename]; @@ -161,7 +162,7 @@ private static function extractStandardAnnotationParams(array $annotations, stri $swaggerType = self::getSwaggerType($annotationType); $nullable = self::isDatatypeNullable($annotationType); - ///TODO: how to find out the correct query type? + // the array element type cannot be determined from standard @param annotations $nestedArraySwaggerType = null; $descriptor = new AnnotationParameterData( From 184bfe1c8cc1e7cdb1a90a0b5c704fcc42dd1967 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 11:19:21 +0100 Subject: [PATCH 060/103] bool validator now supports 1 and 0 as well --- app/helpers/MetaFormats/Validators/VBool.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VBool.php b/app/helpers/MetaFormats/Validators/VBool.php index b45747d47..216c7d088 100644 --- a/app/helpers/MetaFormats/Validators/VBool.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -12,7 +12,11 @@ class VBool public function validate(mixed $value) { - // support stringified values as well - return MetaFormatHelper::checkType($value, PhpTypes::Bool) || $value == "true" || $value == "false"; + // support stringified values as well as 0 and 1 + return MetaFormatHelper::checkType($value, PhpTypes::Bool) + || $value == 0 + || $value == 1 + || $value == "true" + || $value == "false"; } } From 8c358c49c6ab4cf35e938831315c55842c78b3d1 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 11:53:52 +0100 Subject: [PATCH 061/103] swagger generator extracts data only from attributes now --- app/helpers/Swagger/AnnotationHelper.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index 589fb0399..a69471807 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -294,12 +294,10 @@ public static function extractAnnotationData(string $className, string $methodNa $methodAnnotations = self::getMethodAnnotations($className, $methodName); $httpMethod = self::extractAnnotationHttpMethod($methodAnnotations); - $standardAnnotationParams = self::extractStandardAnnotationParams($methodAnnotations, $route); $attributeData = MetaFormatHelper::extractRequestParamData(self::getMethod($className, $methodName)); - $attributeParams = array_map(function ($data) { + $params = array_map(function ($data) { return $data->toAnnotationParameterData(); }, $attributeData); - $params = array_merge($standardAnnotationParams, $attributeParams); $pathParams = []; $queryParams = []; From a28887cc36ffb7079fa02ff872400328c28d087d Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 11:58:19 +0100 Subject: [PATCH 062/103] implemented VTimestamp --- app/helpers/MetaFormats/Validators/VTimestamp.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VTimestamp.php b/app/helpers/MetaFormats/Validators/VTimestamp.php index 4c1424689..493d14a9f 100644 --- a/app/helpers/MetaFormats/Validators/VTimestamp.php +++ b/app/helpers/MetaFormats/Validators/VTimestamp.php @@ -5,13 +5,13 @@ use App\Helpers\MetaFormats\PhpTypes; use App\Helpers\MetaFormats\PrimitiveFormatValidators; -class VTimestamp +/** + * Expects unix timestamps. + */ +class VTimestamp extends VInt { - public const SWAGGER_TYPE = "string"; - - public function validate(mixed $value): bool + public function getExampleValue(): string { - ///TODO: check if timestamp - return true; + return "1740135333"; } } From 9bbfd26537e627c0cc5ca5adbc4d6c486eab50c6 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 12:08:16 +0100 Subject: [PATCH 063/103] added more comments to validators --- app/helpers/MetaFormats/Validators/VArray.php | 6 +++--- app/helpers/MetaFormats/Validators/VBool.php | 4 +++- app/helpers/MetaFormats/Validators/VDouble.php | 4 +++- app/helpers/MetaFormats/Validators/VEmail.php | 7 ++++--- app/helpers/MetaFormats/Validators/VInt.php | 5 +++-- app/helpers/MetaFormats/Validators/VMixed.php | 5 ++--- app/helpers/MetaFormats/Validators/VString.php | 13 +++++++++++++ app/helpers/MetaFormats/Validators/VTimestamp.php | 5 +---- app/helpers/MetaFormats/Validators/VUuid.php | 6 +++--- 9 files changed, 35 insertions(+), 20 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VArray.php b/app/helpers/MetaFormats/Validators/VArray.php index aa72f916d..36690154a 100644 --- a/app/helpers/MetaFormats/Validators/VArray.php +++ b/app/helpers/MetaFormats/Validators/VArray.php @@ -2,9 +2,9 @@ namespace App\Helpers\MetaFormats\Validators; -use App\Helpers\MetaFormats\PhpTypes; -use App\Helpers\MetaFormats\PrimitiveFormatValidators; - +/** + * Validates arrays and their nested elements. + */ class VArray { public const SWAGGER_TYPE = "array"; diff --git a/app/helpers/MetaFormats/Validators/VBool.php b/app/helpers/MetaFormats/Validators/VBool.php index 216c7d088..00dcdae4c 100644 --- a/app/helpers/MetaFormats/Validators/VBool.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -4,8 +4,10 @@ use App\Helpers\MetaFormats\MetaFormatHelper; use App\Helpers\MetaFormats\PhpTypes; -use App\Helpers\MetaFormats\PrimitiveFormatValidators; +/** + * Validates boolean values. Accepts bools, "true", "false", 0 and 1. + */ class VBool { public const SWAGGER_TYPE = "boolean"; diff --git a/app/helpers/MetaFormats/Validators/VDouble.php b/app/helpers/MetaFormats/Validators/VDouble.php index 97e404ffc..d9600c57c 100644 --- a/app/helpers/MetaFormats/Validators/VDouble.php +++ b/app/helpers/MetaFormats/Validators/VDouble.php @@ -4,8 +4,10 @@ use App\Helpers\MetaFormats\MetaFormatHelper; use App\Helpers\MetaFormats\PhpTypes; -use App\Helpers\MetaFormats\PrimitiveFormatValidators; +/** + * Validates doubles. Accepts doubles as well as their stringified versions. + */ class VDouble { public const SWAGGER_TYPE = "number"; diff --git a/app/helpers/MetaFormats/Validators/VEmail.php b/app/helpers/MetaFormats/Validators/VEmail.php index 73139d5fb..e9577465b 100644 --- a/app/helpers/MetaFormats/Validators/VEmail.php +++ b/app/helpers/MetaFormats/Validators/VEmail.php @@ -2,13 +2,14 @@ namespace App\Helpers\MetaFormats\Validators; -use App\Helpers\MetaFormats\PhpTypes; -use App\Helpers\MetaFormats\PrimitiveFormatValidators; - +/** + * Validates emails. + */ class VEmail extends VString { public function __construct() { + // the email should not be empty parent::__construct(1); } diff --git a/app/helpers/MetaFormats/Validators/VInt.php b/app/helpers/MetaFormats/Validators/VInt.php index 5ef10b2e8..7d5f08752 100644 --- a/app/helpers/MetaFormats/Validators/VInt.php +++ b/app/helpers/MetaFormats/Validators/VInt.php @@ -2,11 +2,12 @@ namespace App\Helpers\MetaFormats\Validators; -use App\Exceptions\InternalServerException; use App\Helpers\MetaFormats\MetaFormatHelper; use App\Helpers\MetaFormats\PhpTypes; -use App\Helpers\MetaFormats\PrimitiveFormatValidators; +/** + * Validates integers. Accepts ints as well as their stringified versions. + */ class VInt { public const SWAGGER_TYPE = "integer"; diff --git a/app/helpers/MetaFormats/Validators/VMixed.php b/app/helpers/MetaFormats/Validators/VMixed.php index 04b7fee3c..bf3fd1754 100644 --- a/app/helpers/MetaFormats/Validators/VMixed.php +++ b/app/helpers/MetaFormats/Validators/VMixed.php @@ -2,11 +2,10 @@ namespace App\Helpers\MetaFormats\Validators; -use App\Helpers\MetaFormats\MetaFormatHelper; -use App\Helpers\MetaFormats\PhpTypes; - /** + * Accepts everything. * 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 { diff --git a/app/helpers/MetaFormats/Validators/VString.php b/app/helpers/MetaFormats/Validators/VString.php index 635503fbe..e546ebc6b 100644 --- a/app/helpers/MetaFormats/Validators/VString.php +++ b/app/helpers/MetaFormats/Validators/VString.php @@ -5,6 +5,9 @@ use App\Helpers\MetaFormats\MetaFormatHelper; use App\Helpers\MetaFormats\PhpTypes; +/** + * Validates strings. + */ class VString { public const SWAGGER_TYPE = "string"; @@ -12,6 +15,13 @@ class VString private int $maxLength; private ?string $regex; + /** + * Constructs a string validator. + * @param int $minLength The minimal length of the string. + * @param int $maxLength The maximal length of the string, or -1 for unlimited length. + * @param mixed $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) { $this->minLength = $minLength; @@ -26,10 +36,12 @@ public function getExampleValue() public function validate(mixed $value): bool { + // do not allow other types if (!MetaFormatHelper::checkType($value, PhpTypes::String)) { return false; } + // check length $length = strlen($value); if ($length < $this->minLength) { return false; @@ -38,6 +50,7 @@ public function validate(mixed $value): bool return false; } + // check regex if ($this->regex === null) { return true; } diff --git a/app/helpers/MetaFormats/Validators/VTimestamp.php b/app/helpers/MetaFormats/Validators/VTimestamp.php index 493d14a9f..7013c61ec 100644 --- a/app/helpers/MetaFormats/Validators/VTimestamp.php +++ b/app/helpers/MetaFormats/Validators/VTimestamp.php @@ -2,11 +2,8 @@ namespace App\Helpers\MetaFormats\Validators; -use App\Helpers\MetaFormats\PhpTypes; -use App\Helpers\MetaFormats\PrimitiveFormatValidators; - /** - * Expects unix timestamps. + * Validates unix timestamps. */ class VTimestamp extends VInt { diff --git a/app/helpers/MetaFormats/Validators/VUuid.php b/app/helpers/MetaFormats/Validators/VUuid.php index b2380d642..ae5f6d0ee 100644 --- a/app/helpers/MetaFormats/Validators/VUuid.php +++ b/app/helpers/MetaFormats/Validators/VUuid.php @@ -2,9 +2,9 @@ namespace App\Helpers\MetaFormats\Validators; -use App\Helpers\MetaFormats\PhpTypes; -use App\Helpers\MetaFormats\PrimitiveFormatValidators; - +/** + * Validates UUIDv4. + */ class VUuid extends VString { public function __construct() From a20fafdec493dc7179f98a3adc5fce3f5b2a3a39 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 12:29:21 +0100 Subject: [PATCH 064/103] removed circular dependency of the annotation converter on the produced attributes --- app/commands/SwaggerAnnotator.php | 3 +- .../StandardAnnotationConverter.php | 2 +- app/helpers/Swagger/AnnotationHelper.php | 73 +++++++++++++------ 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/app/commands/SwaggerAnnotator.php b/app/commands/SwaggerAnnotator.php index 5799f7253..5f052d536 100644 --- a/app/commands/SwaggerAnnotator.php +++ b/app/commands/SwaggerAnnotator.php @@ -43,10 +43,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $routesMetadata = AnnotationHelper::getRoutesMetadata(); foreach ($routesMetadata as $route) { // extract data from the existing annotations - $annotationData = AnnotationHelper::extractAnnotationData( + $annotationData = AnnotationHelper::extractAttributeData( $route["class"], $route['method'], - $route["route"] ); // add an empty method to the file with the transpiled annotations diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php index 75dd1feea..74d7b4504 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -87,7 +87,7 @@ private static function convertEndpointAnnotations(array $endpoints, array $line $annotationReplacements = []; foreach ($endpoints as $endpoint) { // get info about endpoint parameters and their types - $annotationData = AnnotationHelper::extractAnnotationData( + $annotationData = AnnotationHelper::extractStandardAnnotationData( $endpoint["class"], $endpoint["method"], $endpoint["route"] diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index a69471807..6cad233d1 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -279,26 +279,11 @@ private static function extractAnnotationDescription(array $annotations): ?strin return null; } - /** - * Extracts the annotation data of an endpoint. The data contains request parameters based on their type - * and the HTTP method. - * @param string $className The name of the containing class. - * @param string $methodName The name of the endpoint method. - * @param string $route The route to the method. - * @throws Exception Thrown when the parser encounters an unknown parameter location (known locations are - * path, query and post) - * @return \App\Helpers\Swagger\AnnotationData Returns a data object containing the parameters and HTTP method. - */ - public static function extractAnnotationData(string $className, string $methodName, string $route): AnnotationData - { - $methodAnnotations = self::getMethodAnnotations($className, $methodName); - - $httpMethod = self::extractAnnotationHttpMethod($methodAnnotations); - $attributeData = MetaFormatHelper::extractRequestParamData(self::getMethod($className, $methodName)); - $params = array_map(function ($data) { - return $data->toAnnotationParameterData(); - }, $attributeData); - + private static function annotationParameterDataToAnnotationData( + HttpMethods $method, + array $params, + ?string $description + ): AnnotationData { $pathParams = []; $queryParams = []; $bodyParams = []; @@ -315,10 +300,54 @@ public static function extractAnnotationData(string $className, string $methodNa } } + return new AnnotationData($method, $pathParams, $queryParams, $bodyParams, $description); + } + + /** + * Extracts standard (@param) annotation data of an endpoint. The data contains request parameters based + * on their type and the HTTP method. + * @param string $className The name of the containing class. + * @param string $methodName The name of the endpoint method. + * @param string $route The route to the method. + * @throws Exception Thrown when the parser encounters an unknown parameter location (known locations are + * path, query and post) + * @return \App\Helpers\Swagger\AnnotationData Returns a data object containing the parameters and HTTP method. + */ + public static function extractStandardAnnotationData( + string $className, + string $methodName, + string $route + ): AnnotationData { + $methodAnnotations = self::getMethodAnnotations($className, $methodName); + + $httpMethod = self::extractAnnotationHttpMethod($methodAnnotations); + $params = self::extractStandardAnnotationParams($methodAnnotations, $route); + $description = self::extractAnnotationDescription($methodAnnotations); + + return self::annotationParameterDataToAnnotationData($httpMethod, $params, $description); + } + + /** + * Extracts the attribute data of an endpoint. The data contains request parameters based on their type + * and the HTTP method. + * @param string $className The name of the containing class. + * @param string $methodName The name of the endpoint method. + * @throws Exception Thrown when the parser encounters an unknown parameter location (known locations are + * path, query and post) + * @return \App\Helpers\Swagger\AnnotationData Returns a data object containing the parameters and HTTP method. + */ + public static function extractAttributeData(string $className, string $methodName): AnnotationData + { + $methodAnnotations = self::getMethodAnnotations($className, $methodName); + + $httpMethod = self::extractAnnotationHttpMethod($methodAnnotations); + $attributeData = MetaFormatHelper::extractRequestParamData(self::getMethod($className, $methodName)); + $params = array_map(function ($data) { + return $data->toAnnotationParameterData(); + }, $attributeData); $description = self::extractAnnotationDescription($methodAnnotations); - $data = new AnnotationData($httpMethod, $pathParams, $queryParams, $bodyParams, $description); - return $data; + return self::annotationParameterDataToAnnotationData($httpMethod, $params, $description); } /** From a165107795d20f3b61fd741e88d70080a74682b1 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 12:44:40 +0100 Subject: [PATCH 065/103] checkedAssign now explicitly throws when an invalid value is passed, resolved some todos --- .../presenters/base/BasePresenter.php | 6 ++---- app/helpers/MetaFormats/MetaFormat.php | 21 +++++++++---------- app/helpers/MetaFormats/RequestParamData.php | 10 ++++----- app/helpers/Swagger/AnnotationData.php | 2 +- .../Swagger/AnnotationParameterData.php | 1 - 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index b64686ea0..ec1135519 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -273,10 +273,8 @@ private function processParamsFormat(string $format) $value = $this->getValueFromParamData($requestParamData); - if (!$formatInstance->checkedAssign($fieldName, $value)) { - ///TODO: it would be nice to give a more detailed error message here - throw new InvalidArgumentException($fieldName); - } + // this throws if the value is invalid + $formatInstance->checkedAssign($fieldName, $value); } // validate structural constraints diff --git a/app/helpers/MetaFormats/MetaFormat.php b/app/helpers/MetaFormats/MetaFormat.php index 3093643a0..ece7ea7e8 100644 --- a/app/helpers/MetaFormats/MetaFormat.php +++ b/app/helpers/MetaFormats/MetaFormat.php @@ -10,13 +10,14 @@ class MetaFormat { /** - * Checks whether the value can be assigned to a field. + * Checks whether the value can be assigned to a field. If not, an exception is thrown. + * The method has no return value. * @param string $fieldName The name of the field. * @param mixed $value The value to be assigned. * @throws \App\Exceptions\InternalServerException Thrown when the field was not found. - * @return bool Returns whether the value conforms to the type and format of the field. + * @throws \App\Exceptions\InvalidArgumentException Thrown when the value is not assignable. */ - public function checkIfAssignable(string $fieldName, mixed $value): bool + public function checkIfAssignable(string $fieldName, mixed $value) { $fieldFormats = FormatCache::getFieldDefinitions(get_class($this)); if (!array_key_exists($fieldName, $fieldFormats)) { @@ -24,23 +25,21 @@ public function checkIfAssignable(string $fieldName, mixed $value): bool } // get the definition for the specific field $formatDefinition = $fieldFormats[$fieldName]; - return $formatDefinition->conformsToDefinition($value); + $formatDefinition->conformsToDefinition($value); } /** - * Tries to assign a value to a field. If the value does not conform to the field format, it will not be assigned. + * Tries to assign a value to a field. If the value does not conform to the field format, an exception is thrown. + * The exception details why the value does not conform to the format. * @param string $fieldName The name of the field. * @param mixed $value The value to be assigned. - * @return bool Returns whether the value was assigned. + * @throws \App\Exceptions\InternalServerException Thrown when the field was not found. + * @throws \App\Exceptions\InvalidArgumentException Thrown when the value is not assignable. */ public function checkedAssign(string $fieldName, mixed $value) { - if (!$this->checkIfAssignable($fieldName, $value)) { - return false; - } - + $this->checkIfAssignable($fieldName, $value); $this->$fieldName = $value; - return true; } /** diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index fac2e8bbe..0943e3f79 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -35,10 +35,10 @@ public function __construct( } /** - * Checks whether a value meets this definition. + * Checks whether a value meets this definition. If the definition is not met, an exception is thrown. + * The method has no return value. * @param mixed $value The value to be checked. * @throws \App\Exceptions\InvalidArgumentException Thrown when the value does not meet the definition. - * @return bool Returns whether the value passed the test. */ public function conformsToDefinition(mixed $value) { @@ -46,7 +46,7 @@ public function conformsToDefinition(mixed $value) if ($value === null) { // optional parameters can be null if (!$this->required) { - return true; + return; } // required parameters can be null only if explicitly nullable @@ -59,7 +59,7 @@ public function conformsToDefinition(mixed $value) // only non null values should be validated // (validators do not expect null) - return true; + return; } // use every provided validator @@ -72,8 +72,6 @@ public function conformsToDefinition(mixed $value) ); } } - - return true; } private function hasValidators(): bool diff --git a/app/helpers/Swagger/AnnotationData.php b/app/helpers/Swagger/AnnotationData.php index 31c781bd1..594804233 100644 --- a/app/helpers/Swagger/AnnotationData.php +++ b/app/helpers/Swagger/AnnotationData.php @@ -57,7 +57,7 @@ private function getBodyAnnotation(): string | null return null; } - ///TODO: The swagger generator only supports JSON due to the hardcoded mediaType below + // only json is supported due to the media type $head = '@OA\RequestBody(@OA\MediaType(mediaType="application/json",@OA\Schema'; $body = new ParenthesesBuilder(); diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index 633fd076b..e94c1f5ad 100644 --- a/app/helpers/Swagger/AnnotationParameterData.php +++ b/app/helpers/Swagger/AnnotationParameterData.php @@ -102,7 +102,6 @@ public function toPropertyAnnotation(): string $head = "@OA\\Property"; $body = new ParenthesesBuilder(); - ///TODO: Once the meta-view formats are implemented, add support for property nullability here. $body->addKeyValue("property", $this->name); $body->addKeyValue("type", $this->swaggerType); $body->addKeyValue("nullable", $this->nullable); From 2b61283a087fb54c20fce664072ec28dd0a483e8 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 12:51:06 +0100 Subject: [PATCH 066/103] fixed param comment --- app/helpers/MetaFormats/Validators/VString.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VString.php b/app/helpers/MetaFormats/Validators/VString.php index e546ebc6b..669ed4602 100644 --- a/app/helpers/MetaFormats/Validators/VString.php +++ b/app/helpers/MetaFormats/Validators/VString.php @@ -19,8 +19,8 @@ class VString * Constructs a string validator. * @param int $minLength The minimal length of the string. * @param int $maxLength The maximal length of the string, or -1 for unlimited length. - * @param mixed $regex Regex pattern used for validation. Evaluated with the preg_match function with this argument - * as the pattern. + * @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) { From b724809733aa46c980417a6d8c009b90dde5b2c9 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 13:34:33 +0100 Subject: [PATCH 067/103] added shorthands for format parameter attributes --- .../presenters/RegistrationPresenter.php | 16 +----- app/helpers/MetaFormats/Attributes/FPath.php | 28 ++++++++++ app/helpers/MetaFormats/Attributes/FPost.php | 28 ++++++++++ app/helpers/MetaFormats/Attributes/FQuery.php | 28 ++++++++++ .../Attributes/FormatAttribute.php | 2 +- .../FormatDefinitions/GroupFormat.php | 34 ------------- .../FormatDefinitions/UserFormat.php | 49 +++++++----------- app/helpers/MetaFormats/MetaFormatHelper.php | 51 +++++++++++++------ 8 files changed, 141 insertions(+), 95 deletions(-) create mode 100644 app/helpers/MetaFormats/Attributes/FPath.php create mode 100644 app/helpers/MetaFormats/Attributes/FPost.php create mode 100644 app/helpers/MetaFormats/Attributes/FQuery.php delete mode 100644 app/helpers/MetaFormats/FormatDefinitions/GroupFormat.php diff --git a/app/V1Module/presenters/RegistrationPresenter.php b/app/V1Module/presenters/RegistrationPresenter.php index 6fc89d3b6..2efb68dd3 100644 --- a/app/V1Module/presenters/RegistrationPresenter.php +++ b/app/V1Module/presenters/RegistrationPresenter.php @@ -34,7 +34,7 @@ use App\Helpers\EmailVerificationHelper; use App\Helpers\RegistrationConfig; use App\Helpers\InvitationHelper; -use App\Helpers\MetaFormats\Attributes\FormatAttribute; +use App\Helpers\MetaFormats\Attributes\Format; use App\Helpers\MetaFormats\FormatDefinitions\UserFormat; use App\Helpers\MetaFormats\Attributes\ParamAttribute; use App\Security\Roles; @@ -274,19 +274,7 @@ public function checkCreateInvitation() * @throws BadRequestException * @throws InvalidArgumentException */ - #[Post("email", new VEmail(), "An email that will serve as a login name")] - #[Post("firstName", new VString(2), "First name", required: true)] - #[Post("lastName", new VString(2), "Last name")] - #[Post("instanceId", new VString(1), "Identifier of the instance to register in")] - #[Post("titlesBeforeName", new VString(1), "Titles which is placed before user name", required: false)] - #[Post("titlesAfterName", new VString(1), "Titles which is placed after user name", required: false)] - #[Post( - "groups", - new VArray(), - "List of group IDs in which the user is added right after registration", - required: false, - )] - #[Post("locale", new VString(2, 2), "Language used in the invitation email (en by default).", required: false)] + #[Format(UserFormat::class)] public function actionCreateInvitation() { $req = $this->getRequest(); diff --git a/app/helpers/MetaFormats/Attributes/FPath.php b/app/helpers/MetaFormats/Attributes/FPath.php new file mode 100644 index 000000000..3da3899fa --- /dev/null +++ b/app/helpers/MetaFormats/Attributes/FPath.php @@ -0,0 +1,28 @@ +getAttributes(FormatAttribute::class); + $formatAttributes = $reflectionObject->getAttributes(Format::class); if (count($formatAttributes) === 0) { return null; } @@ -100,11 +103,33 @@ public static function extractRequestParamData(ReflectionMethod $reflectionMetho return $data; } - public static function extractFormatParameterData(ReflectionProperty $reflectionObject): ?RequestParamData + /** + * Finds the format attribute of the property and extracts its data. + * @param \ReflectionProperty $reflectionObject The reflection object of the property. + * @throws \App\Exceptions\InternalServerException Thrown when there is not exactly one format attribute. + * @return RequestParamData Returns the data from the attribute. + */ + public static function extractFormatParameterData(ReflectionProperty $reflectionObject): RequestParamData { - $requestAttributes = $reflectionObject->getAttributes(FormatParameterAttribute::class); - if (count($requestAttributes) === 0) { - return null; + // find all property attributes + $longAttributes = $reflectionObject->getAttributes(FormatParameterAttribute::class); + $pathAttribues = $reflectionObject->getAttributes(FPath::class); + $queryAttributes = $reflectionObject->getAttributes(FQuery::class); + $postAttributes = $reflectionObject->getAttributes(FPost::class); + $requestAttributes = array_merge($longAttributes, $pathAttribues, $queryAttributes, $postAttributes); + + // there should be only one attribute + if (count($requestAttributes) == 0) { + throw new InternalServerException( + "The field {$reflectionObject->name} of " + . "class {$reflectionObject->class} does not have a property attribute." + ); + } + if (count($requestAttributes) > 1) { + throw new InternalServerException( + "The field {$reflectionObject->name} of " + . "class {$reflectionObject->class} has more than one attribute." + ); } $requestAttribute = $requestAttributes[0]->newInstance(); @@ -142,18 +167,12 @@ public static function debugGetAttributes( */ public static function createNameToFieldDefinitionsMap(string $className): array { - $class = new ReflectionClass($className); + $class = new ReflectionClass(objectOrClass: $className); $fields = get_class_vars($className); $formats = []; foreach ($fields as $fieldName => $value) { $field = $class->getProperty($fieldName); $requestParamData = self::extractFormatParameterData($field); - if ($requestParamData === null) { - throw new InternalServerException( - "The field $fieldName of class $className does not have a RequestAttribute." - ); - } - $formats[$fieldName] = $requestParamData; } From 9a850fc28f215868fe42dbe0d94966464366e9b7 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 13:52:32 +0100 Subject: [PATCH 068/103] removed MetaRequest, refactored actionCreateInvitation to use a format class --- .../presenters/RegistrationPresenter.php | 19 ++-- .../presenters/base/BasePresenter.php | 15 +-- app/helpers/MetaFormats/MetaRequest.php | 93 ------------------- 3 files changed, 14 insertions(+), 113 deletions(-) delete mode 100644 app/helpers/MetaFormats/MetaRequest.php diff --git a/app/V1Module/presenters/RegistrationPresenter.php b/app/V1Module/presenters/RegistrationPresenter.php index 2efb68dd3..ca281ef07 100644 --- a/app/V1Module/presenters/RegistrationPresenter.php +++ b/app/V1Module/presenters/RegistrationPresenter.php @@ -277,16 +277,17 @@ public function checkCreateInvitation() #[Format(UserFormat::class)] public function actionCreateInvitation() { - $req = $this->getRequest(); + /** @var UserFormat */ + $format = $this->getFormatInstance(); // check if the email is free - $email = trim($req->getPost("email")); + $email = trim($format->email); // username is name of column which holds login identifier represented by email if ($this->logins->getByUsername($email) !== null) { throw new BadRequestException("This email address is already taken."); } - $groupsIds = $req->getPost("groups") ?? []; + $groupsIds = $format->groups ?? []; foreach ($groupsIds as $id) { $group = $this->groups->get($id); if (!$group || $group->isOrganizational() || !$this->groupAcl->canInviteStudents($group)) { @@ -295,23 +296,23 @@ public function actionCreateInvitation() } // gather data - $instanceId = $req->getPost("instanceId"); + $instanceId = $format->instanceId; $instance = $this->getInstance($instanceId); - $titlesBeforeName = $req->getPost("titlesBeforeName") === null ? "" : $req->getPost("titlesBeforeName"); - $titlesAfterName = $req->getPost("titlesAfterName") === null ? "" : $req->getPost("titlesAfterName"); + $titlesBeforeName = $format->titlesBeforeName === null ? "" : $format->titlesBeforeName; + $titlesAfterName = $format->titlesAfterName === null ? "" : $format->titlesAfterName; // create the token and send it via email try { $this->invitationHelper->invite( $instanceId, $email, - $req->getPost("firstName"), - $req->getPost("lastName"), + $format->firstName, + $format->lastName, $titlesBeforeName, $titlesAfterName, $groupsIds, $this->getCurrentUser(), - $req->getPost("locale") ?? "en", + $format->locale ?? "en", ); } catch (InvalidAccessTokenException $e) { throw new BadRequestException( diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index ec1135519..7a8f1b088 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -75,8 +75,8 @@ class BasePresenter extends \App\Presenters\BasePresenter */ public $logger; - /** @var mixed Processed parameters from the request (MetaFormat or stdClass) */ - private mixed $requestFormatInstance; + /** @var MetaFormat Instance of the meta format used by the endpoint (null if no format used) */ + private MetaFormat $requestFormatInstance; protected function formatPermissionCheckMethod($action) { @@ -204,16 +204,9 @@ protected function isInScope(string $scope): bool return $identity->isInScope($scope); } - public function getMetaRequest(): MetaRequest | null + public function getFormatInstance(): MetaFormat { - if ($this->requestFormatInstance === null) { - throw new InternalServerException( - "getMetaRequest() cannot be used without a format class defined for the endpoint." - ); - } - - $request = parent::getRequest(); - return new MetaRequest($request, $this->requestFormatInstance); + return $this->requestFormatInstance; } private function processParams(ReflectionMethod $reflection) diff --git a/app/helpers/MetaFormats/MetaRequest.php b/app/helpers/MetaFormats/MetaRequest.php deleted file mode 100644 index fba9ccc28..000000000 --- a/app/helpers/MetaFormats/MetaRequest.php +++ /dev/null @@ -1,93 +0,0 @@ -baseRequest = $request; - $this->requestFormatInstance = $requestFormatInstance; - } - - /** - * Retrieve the presenter name. - */ - public function getPresenterName(): string - { - return $this->baseRequest->getPresenterName(); - } - - /** - * Returns all variables provided to the presenter (usually via URL). - */ - public function getParameters(): array - { - return $this->baseRequest->getParameters(); - } - - /** - * Returns a parameter provided to the presenter. - */ - public function getParameter(string $key): mixed - { - return $this->baseRequest->getParameter($key); - } - - /** - * Returns a variable provided to the presenter via POST. - * If no key is passed, returns the entire array. - */ - public function getPost(?string $key = null): mixed - { - if ($key === null) { - return $this->requestFormatInstance; - } - - return $this->requestFormatInstance->$key; - } - - /** - * Returns all uploaded files. - */ - public function getFiles(): array - { - return $this->baseRequest->getFiles(); - } - - /** - * Returns the method. - */ - public function getMethod(): ?string - { - return $this->baseRequest->getMethod(); - } - - - /** - * Checks if the method is the given one. - */ - public function isMethod(string $method): bool - { - return $this->baseRequest->isMethod($method); - } - - /** - * Checks the flag. - */ - public function hasFlag(string $flag): bool - { - return $this->baseRequest->hasFlag($flag); - } - - - public function toArray(): array - { - return $this->baseRequest->toArray(); - } -} From 9bf3a443935075dd162a206f27f5f804f5afa8da Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 13:58:38 +0100 Subject: [PATCH 069/103] bugfix: loose params are no longer saved --- app/V1Module/presenters/base/BasePresenter.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/V1Module/presenters/base/BasePresenter.php b/app/V1Module/presenters/base/BasePresenter.php index 7a8f1b088..b50d3c1ec 100644 --- a/app/V1Module/presenters/base/BasePresenter.php +++ b/app/V1Module/presenters/base/BasePresenter.php @@ -225,8 +225,6 @@ private function processParams(ReflectionMethod $reflection) private function processParamsLoose(array $paramData) { - $formatInstanceArr = []; - // validate each param foreach ($paramData as $param) { ///TODO: path parameters are not checked yet @@ -235,14 +233,10 @@ private function processParamsLoose(array $paramData) } $paramValue = $this->getValueFromParamData($param); - $formatInstanceArr[$param->name] = $paramValue; // this throws when it does not conform $param->conformsToDefinition($paramValue); } - - // cast to stdClass - $this->requestFormatInstance = (object)$formatInstanceArr; } private function processParamsFormat(string $format) From 4e96b1fe0d1d94fd0a286086bcd9339472908108 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 14:29:22 +0100 Subject: [PATCH 070/103] resolved todo --- app/helpers/Swagger/AnnotationHelper.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/helpers/Swagger/AnnotationHelper.php b/app/helpers/Swagger/AnnotationHelper.php index 6cad233d1..ebf7829c5 100644 --- a/app/helpers/Swagger/AnnotationHelper.php +++ b/app/helpers/Swagger/AnnotationHelper.php @@ -17,7 +17,6 @@ */ class AnnotationHelper { - ///TODO: the null might be a prefix as well private static $nullableSuffix = '|null'; private static $typeMap = [ 'bool' => 'boolean', @@ -134,8 +133,7 @@ private static function extractStandardAnnotationParams(array $annotations, stri { $routeParams = self::getRoutePathParamNames($route); - ///TODO: there can be unannotated query params as well - + // does not see unannotated query params, but there are not any $params = []; foreach ($annotations as $annotation) { // assumed that all query parameters have a @param annotation From 449d164cfe06d018b5258470d2874fbb7c035302 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 15:03:52 +0100 Subject: [PATCH 071/103] refactored the FormatCache --- app/helpers/MetaFormats/FormatCache.php | 68 ++++++++++---------- app/helpers/MetaFormats/MetaFormatHelper.php | 27 ++------ 2 files changed, 39 insertions(+), 56 deletions(-) diff --git a/app/helpers/MetaFormats/FormatCache.php b/app/helpers/MetaFormats/FormatCache.php index d5990b3a2..6b6bd81cd 100644 --- a/app/helpers/MetaFormats/FormatCache.php +++ b/app/helpers/MetaFormats/FormatCache.php @@ -4,61 +4,59 @@ use App\Exceptions\InternalServerException; +/** + * Cache for various format related data. + * Acts as a singleton storage because all of the cached data is static. + */ class FormatCache { - private static ?array $formatToClassMap = null; - private static ?array $classToFormatMap = null; + private static ?array $formatNames = null; private static ?array $formatToFieldFormatsMap = null; - public static function getFormatToClassMap(): array - { - if (self::$formatToClassMap == null) { - self::$formatToClassMap = MetaFormatHelper::createFormatToClassMap(); - } - return self::$formatToClassMap; - } - - public static function getClassToFormatMap(): array - { - if (self::$classToFormatMap == null) { - self::$classToFormatMap = []; - $formatToClassMap = self::getFormatToClassMap(); - foreach ($formatToClassMap as $format => $class) { - self::$classToFormatMap[$class] = $format; - } - } - return self::$classToFormatMap; - } - + /** + * @return array Returns a dictionary of dictionaries: [ => [ => RequestParamData, ...], ...] + * mapping formats to their fields and field metadata. + */ public static function getFormatToFieldDefinitionsMap(): array { if (self::$formatToFieldFormatsMap == null) { self::$formatToFieldFormatsMap = []; - $formatToClassMap = self::getFormatToClassMap(); - foreach ($formatToClassMap as $format => $class) { - self::$formatToFieldFormatsMap[$format] = MetaFormatHelper::createNameToFieldDefinitionsMap($class); + $formatNames = self::getFormatNames(); + foreach ($formatNames as $format) { + self::$formatToFieldFormatsMap[$format] = MetaFormatHelper::createNameToFieldDefinitionsMap($format); } } return self::$formatToFieldFormatsMap; } - public static function getFormatFieldNames(string $format): array + /** + * @return array Returns an array of all defined formats. + */ + public static function getFormatNames(): array { - $formatToFieldDefinitionsMap = self::getFormatToFieldDefinitionsMap(); - if (!array_key_exists($format, $formatToFieldDefinitionsMap)) { - throw new InternalServerException("The format $format does not have a field format definition."); + if (self::$formatNames == null) { + self::$formatNames = MetaFormatHelper::createFormatNamesArray(); } - return array_keys($formatToFieldDefinitionsMap[$format]); + return self::$formatNames; + } + + public static function formatExists(string $format): bool + { + return in_array($format, self::getFormatNames()); } - public static function getFieldDefinitions(string $className) + /** + * Fetches field metadata for the given format. + * @param string $format The name of the format. + * @throws \App\Exceptions\InternalServerException Thrown when the format is corrupted. + * @return array Returns a dictionary of field names to RequestParamData. + */ + public static function getFieldDefinitions(string $format) { - $classToFormatMap = self::getClassToFormatMap(); - if (!array_key_exists($className, $classToFormatMap)) { - throw new InternalServerException("The class $className does not have a format definition."); + if (!self::formatExists($format)) { + throw new InternalServerException("The class $format does not have a format definition."); } - $format = $classToFormatMap[$className]; $formatToFieldFormatsMap = self::getFormatToFieldDefinitionsMap(); if (!array_key_exists($format, $formatToFieldFormatsMap)) { throw new InternalServerException("The format $format does not have a field format definition."); diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index ccbcd72d8..a36ed2d39 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -180,9 +180,9 @@ public static function createNameToFieldDefinitionsMap(string $className): array } /** - * Creates a mapping from formats to class names, where the class defines the format. + * Finds all defined formats and returns an array of their names. */ - public static function createFormatToClassMap() + public static function createFormatNamesArray() { // scan directory of format definitions $formatFiles = scandir(self::$formatDefinitionFolder); @@ -195,21 +195,8 @@ public static function createFormatToClassMap() return self::$formatDefinitionsNamespace . "\\$fileWithoutExtension"; }, $formatFiles); - // maps format names to class names - $formatClassMap = []; - - foreach ($classes as $className) { - // get the format attribute - $class = new ReflectionClass($className); - $format = self::extractFormatFromAttribute($class); - if ($format === null) { - throw new InternalServerException("The class {$className} does not have the format attribute."); - } - - $formatClassMap[$format] = $className; - } - - return $formatClassMap; + // formats are just class names + return array_values($classes); } /** @@ -220,13 +207,11 @@ public static function createFormatToClassMap() */ public static function createFormatInstance(string $format): MetaFormat { - $formatToClassMap = FormatCache::getFormatToClassMap(); - if (!array_key_exists($format, $formatToClassMap)) { + if (!FormatCache::formatExists($format)) { throw new InternalServerException("The format $format does not exist."); } - $className = $formatToClassMap[$format]; - $instance = new $className(); + $instance = new $format(); return $instance; } From 051247dad8b86cc2f0b4fb78704c8ac4c5f44823 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 21 Feb 2025 15:09:15 +0100 Subject: [PATCH 072/103] code cleanup --- app/helpers/MetaFormats/FormatParser.php | 37 ------------------- app/helpers/MetaFormats/MetaFormat.php | 3 -- app/helpers/MetaFormats/MetaFormatHelper.php | 16 ++++---- app/helpers/MetaFormats/RequestParamData.php | 11 +++++- .../Swagger/AnnotationParameterData.php | 1 + 5 files changed, 18 insertions(+), 50 deletions(-) delete mode 100644 app/helpers/MetaFormats/FormatParser.php diff --git a/app/helpers/MetaFormats/FormatParser.php b/app/helpers/MetaFormats/FormatParser.php deleted file mode 100644 index a0ef830ae..000000000 --- a/app/helpers/MetaFormats/FormatParser.php +++ /dev/null @@ -1,37 +0,0 @@ -nullable = true; - $format = substr($format, 0, -1); - } - - // check array - if (str_ends_with($format, "[]")) { - $this->isArray = true; - $format = substr($format, 0, -2); - $this->nested = new FormatParser($format); - } else { - $this->format = $format; - } - } -} diff --git a/app/helpers/MetaFormats/MetaFormat.php b/app/helpers/MetaFormats/MetaFormat.php index ece7ea7e8..a3a508a41 100644 --- a/app/helpers/MetaFormats/MetaFormat.php +++ b/app/helpers/MetaFormats/MetaFormat.php @@ -3,9 +3,6 @@ namespace App\Helpers\MetaFormats; use App\Exceptions\InternalServerException; -use App\Helpers\Swagger\AnnotationHelper; - -use function Symfony\Component\String\b; class MetaFormat { diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index a36ed2d39..eaa88fe48 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -160,11 +160,11 @@ public static function debugGetAttributes( return $data; } - /** - * Parses the format attributes of class fields and returns their metadata. - * @param string $className The name of the class. - * @return array Returns a dictionary with the field name as the key and RequestParamData as the value. - */ + /** + * Parses the format attributes of class fields and returns their metadata. + * @param string $className The name of the class. + * @return array Returns a dictionary with the field name as the key and RequestParamData as the value. + */ public static function createNameToFieldDefinitionsMap(string $className): array { $class = new ReflectionClass(objectOrClass: $className); @@ -179,9 +179,9 @@ public static function createNameToFieldDefinitionsMap(string $className): array return $formats; } - /** - * Finds all defined formats and returns an array of their names. - */ + /** + * Finds all defined formats and returns an array of their names. + */ public static function createFormatNamesArray() { // scan directory of format definitions diff --git a/app/helpers/MetaFormats/RequestParamData.php b/app/helpers/MetaFormats/RequestParamData.php index 0943e3f79..ad1e44349 100644 --- a/app/helpers/MetaFormats/RequestParamData.php +++ b/app/helpers/MetaFormats/RequestParamData.php @@ -5,10 +5,12 @@ use App\Exceptions\InternalServerException; use App\Exceptions\InvalidArgumentException; use App\Helpers\MetaFormats\Validators\VArray; -use App\Helpers\MetaFormats\Validators\VString; use App\Helpers\Swagger\AnnotationParameterData; use Exception; +/** + * Data class containing metadata for request parameters. + */ class RequestParamData { public Type $type; @@ -23,7 +25,7 @@ public function __construct( string $name, string $description, bool $required, - array $validators = [], + array $validators, bool $nullable = false, ) { $this->type = $type; @@ -82,6 +84,11 @@ private function hasValidators(): bool return $this->validators !== null; } + /** + * Converts the metadata into metadata used for swagger generation. + * @throws \App\Exceptions\InternalServerException Thrown when the parameter metadata is corrupted. + * @return AnnotationParameterData Return metadata used for swagger generation. + */ public function toAnnotationParameterData() { if (!$this->hasValidators()) { diff --git a/app/helpers/Swagger/AnnotationParameterData.php b/app/helpers/Swagger/AnnotationParameterData.php index e94c1f5ad..ddd9924f6 100644 --- a/app/helpers/Swagger/AnnotationParameterData.php +++ b/app/helpers/Swagger/AnnotationParameterData.php @@ -6,6 +6,7 @@ /** * Contains data of a single annotation parameter. + * Used for swagger generation. */ class AnnotationParameterData { From dae8c9b25e94ff31b5327356283114b48ee3cb28 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 12:46:32 +0100 Subject: [PATCH 073/103] replaced string slice with str_starts_with --- .../AnnotationConversion/AnnotationToAttributeConverter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php index c1cbb9745..38a7153f1 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php @@ -27,7 +27,7 @@ public static function convertFile(string $path): string $paramTypeClass = Utils::shortenClass(Type::class); foreach (Utils::fileStringToLines($contentWithPlaceholders) as $line) { // detected the initial "use" block, add usings for new types - if (!$usingsAdded && strlen($line) > 3 && substr($line, 0, 3) === "use") { + if (!$usingsAdded && strlen($line) > 3 && str_starts_with($line, "use")) { // add usings for attributes foreach ($paramAttributeClasses as $class) { $lines[] = "use App\\Helpers\\MetaFormats\\Attributes\\{$class};"; From cfa83e5de6e0101b0312ad3ef0b6cc5f70ce7b95 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 12:48:39 +0100 Subject: [PATCH 074/103] inlined function --- .../AnnotationToAttributeConverter.php | 2 +- .../StandardAnnotationConverter.php | 2 +- app/helpers/MetaFormats/AnnotationConversion/Utils.php | 10 ---------- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php index 38a7153f1..e6e11ee98 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php @@ -58,6 +58,6 @@ public static function convertFile(string $path): string } } - return Utils::linesToFileString($lines); + return implode("\n", $lines); } } diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php index 74d7b4504..5313d6690 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -71,7 +71,7 @@ public static function convertStandardAnnotations(string $path): string $i = $annotationReplacements[$i]["originalAnnotationEndLine"]; } - return Utils::linesToFileString($newLines); + return implode("\n", $newLines); } /** diff --git a/app/helpers/MetaFormats/AnnotationConversion/Utils.php b/app/helpers/MetaFormats/AnnotationConversion/Utils.php index de35f0e98..958759579 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/Utils.php +++ b/app/helpers/MetaFormats/AnnotationConversion/Utils.php @@ -48,16 +48,6 @@ public static function fileStringToLines(string $fileContent): array return $lines; } - /** - * Joins an array of strings into a single string separated by '\n'. - * @param array $lines The lines to be joined. - * @return string The joined string. - */ - public static function linesToFileString(array $lines): string - { - return implode("\n", $lines); - } - /** * @return string[] Returns an array of Validator class names (without the namespace). */ From 9b20a7565ebdc200c22e55338e30f1d06113e174 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 13:18:18 +0100 Subject: [PATCH 075/103] replaced switch with translation dictionary --- .../AnnotationConversion/Utils.php | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/Utils.php b/app/helpers/MetaFormats/AnnotationConversion/Utils.php index 958759579..d723373d9 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/Utils.php +++ b/app/helpers/MetaFormats/AnnotationConversion/Utils.php @@ -10,6 +10,13 @@ class Utils { + // links all @Param location types to corresponding attribute classes. + private static array $paramLocationToAttributeClassDictionary = [ + "post" => Post::class, + "query" => Query::class, + "path" => Path::class, + ]; + /** * Converts a fully qualified class name to a class name without namespace prefixes. * @param string $className Fully qualified class name, such @@ -76,16 +83,20 @@ public static function getPresenterNamespace() return $namespace; } + /** + * Returns the attribute class name (without namespace) matching the input parameter location string. + * @param string $type The location type of the parameter (path, query, post). + * @throws \App\Exceptions\InternalServerException Thrown when an unexpected type is provided. + * @return string Returns the attribute class name matching the parameter location type. + */ public static function getAttributeClassFromString(string $type) { - switch ($type) { - case "post": - return self::shortenClass(Post::class); - case "query": - return self::shortenClass(Query::class); - case "path": - return self::shortenClass(Path::class); + if (!array_key_exists($type, self::$paramLocationToAttributeClassDictionary)) { + throw new InternalServerException("Unsupported parameter location: $type"); } + + $className = self::$paramLocationToAttributeClassDictionary[$type]; + return self::shortenClass($className); } public static function getParamAttributeClassNames() From 81f25e5744275edc03fac7578782d4d3a8e3da1b Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 13:30:18 +0100 Subject: [PATCH 076/103] removed hardcoded namespaces where possible --- .../AnnotationConversion/AnnotationToAttributeConverter.php | 6 +++--- app/helpers/MetaFormats/AnnotationConversion/Utils.php | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php index e6e11ee98..516bfbc3e 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/AnnotationToAttributeConverter.php @@ -24,15 +24,15 @@ public static function convertFile(string $path): string $netteAttributeLinesCount = 0; $usingsAdded = false; $paramAttributeClasses = Utils::getParamAttributeClassNames(); - $paramTypeClass = Utils::shortenClass(Type::class); + $paramTypeClass = Type::class; foreach (Utils::fileStringToLines($contentWithPlaceholders) as $line) { // detected the initial "use" block, add usings for new types if (!$usingsAdded && strlen($line) > 3 && str_starts_with($line, "use")) { // add usings for attributes foreach ($paramAttributeClasses as $class) { - $lines[] = "use App\\Helpers\\MetaFormats\\Attributes\\{$class};"; + $lines[] = "use {$class};"; } - $lines[] = "use App\\Helpers\\MetaFormats\\{$paramTypeClass};"; + $lines[] = "use {$paramTypeClass};"; foreach (Utils::getValidatorNames() as $validator) { $lines[] = "use App\\Helpers\\MetaFormats\\Validators\\{$validator};"; } diff --git a/app/helpers/MetaFormats/AnnotationConversion/Utils.php b/app/helpers/MetaFormats/AnnotationConversion/Utils.php index d723373d9..a5cac074f 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/Utils.php +++ b/app/helpers/MetaFormats/AnnotationConversion/Utils.php @@ -101,10 +101,6 @@ public static function getAttributeClassFromString(string $type) public static function getParamAttributeClassNames() { - return [ - self::shortenClass(Post::class), - self::shortenClass(Query::class), - self::shortenClass(Path::class), - ]; + return array_values(self::$paramLocationToAttributeClassDictionary); } } From 9fc4ab5d3a4b08b4c37c7d99bf0fc595c3b584dd Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 14:05:17 +0100 Subject: [PATCH 077/103] added comment --- app/helpers/MetaFormats/AnnotationConversion/Utils.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/helpers/MetaFormats/AnnotationConversion/Utils.php b/app/helpers/MetaFormats/AnnotationConversion/Utils.php index a5cac074f..0974c983a 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/Utils.php +++ b/app/helpers/MetaFormats/AnnotationConversion/Utils.php @@ -99,6 +99,9 @@ public static function getAttributeClassFromString(string $type) return self::shortenClass($className); } + /** + * @return array Returns all parameter attribute class names (including namespace). + */ public static function getParamAttributeClassNames() { return array_values(self::$paramLocationToAttributeClassDictionary); From 971c77ecf2d2afae1a49693ab4131668f3d7a423 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 14:06:06 +0100 Subject: [PATCH 078/103] replaced hardcoded numbers --- .../AnnotationConversion/NetteAnnotationConverter.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index 97b422c15..87cd5882d 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -134,11 +134,14 @@ private static function convertAnnotationValidationToValidatorString(string $val $stringValidator = Utils::shortenClass(VString::class); // handle string length constraints, such as "string:1..255" - if (strlen($validation) > 6) { - if ($validation[6] !== ":") { + $prefixLength = strlen("string"); + if (strlen($validation) > $prefixLength) { + // the 'string' prefix needs to be followed with a colon + if ($validation[$prefixLength] !== ":") { throw new InternalServerException("Unknown string validation format: $validation"); } - $suffix = substr($validation, 7); + // omit the 'string:' section + $suffix = substr($validation, $prefixLength + 1); // special case for uuids if ($suffix === "36") { From 74eba859b222237dc45aaf8b036b0facbf7913d9 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 14:33:14 +0100 Subject: [PATCH 079/103] replaced switch with dictionary --- .../NetteAnnotationConverter.php | 49 +++++++------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index 87cd5882d..a65cb3800 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -30,6 +30,22 @@ class NetteAnnotationConverter // this text must not be present in the presenter files public static string $attributePlaceholder = "#nette#"; + // maps @Param validation fields to validator classes + private static array $netteValidationToValidatorClassDictionary = [ + "email" => VEmail::class, + // there is one occurrence of this + "email:1.." => VEmail::class, + "numericint" => VInt::class, + "integer" => VInt::class, + "bool" => VBool::class, + "boolean" => VBool::class, + "array" => VArray::class, + "list" => VArray::class, + "timestamp" => VTimestamp::class, + "numeric" => VDouble::class, + "mixed" => VMixed::class, + ]; + /** * Replaces "@Param" annotations with placeholders and extracts its data. @@ -175,37 +191,10 @@ private static function convertAnnotationValidationToValidatorString(string $val } // non-string validation rules do not have parameters, so they can be converted directly - $validatorClass = null; - switch ($validation) { - case "email": - // there is one occurrence of this - case "email:1..": - $validatorClass = VEmail::class; - break; - case "numericint": - case "integer": - $validatorClass = VInt::class; - break; - case "bool": - case "boolean": - $validatorClass = VBool::class; - break; - case "array": - case "list": - $validatorClass = VArray::class; - break; - case "timestamp": - $validatorClass = VTimestamp::class; - break; - case "numeric": - $validatorClass = VDouble::class; - break; - case "mixed": - $validatorClass = VMixed::class; - break; - default: - throw new InternalServerException("Unknown validation rule: $validation"); + if (!array_key_exists($validation, self::$netteValidationToValidatorClassDictionary)) { + throw new InternalServerException("Unknown validation rule: $validation"); } + $validatorClass = self::$netteValidationToValidatorClassDictionary[$validation]; return "new " . Utils::shortenClass($validatorClass) . "()"; } From a86d53b418dd078bffeec621c698dcb641d7f01b Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 14:40:28 +0100 Subject: [PATCH 080/103] inlined a function --- .../AnnotationConversion/NetteAnnotationConverter.php | 10 +++++++--- .../MetaFormats/AnnotationConversion/Utils.php | 11 ----------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index a65cb3800..ad9121f30 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -226,11 +226,15 @@ public static function convertRegexCapturesToParenthesesBuilder(array $annotatio } $validation = $annotationParameters["validation"]; - if (Utils::checkValidationNullability($validation)) { - // remove the '|null' from the end of the string - $validation = substr($validation, 0, -5); + // check nullability + // validation strings contain the 'null' qualifier always at the end of the string. + $nullabilitySuffix = "|null"; + if (str_ends_with($validation, $nullabilitySuffix)) { + // remove the '|null' + $validation = substr($validation, 0, -strlen($nullabilitySuffix)); $nullable = true; } + // this will always produce a single validator (the annotations do not contain multiple validation fields) $validator = self::convertAnnotationValidationToValidatorString($validation); $parenthesesBuilder->addValue(value: $validator); diff --git a/app/helpers/MetaFormats/AnnotationConversion/Utils.php b/app/helpers/MetaFormats/AnnotationConversion/Utils.php index 0974c983a..ac83a6a21 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/Utils.php +++ b/app/helpers/MetaFormats/AnnotationConversion/Utils.php @@ -29,17 +29,6 @@ public static function shortenClass(string $className) return end($tokens); } - /** - * Checks whether the validation string ends with the "|null" suffix. - * Validation strings contain the "null" qualifier always at the end of the string. - * @param string $validation The validation string. - * @return bool Returns whether the validation ends with "|null". - */ - public static function checkValidationNullability(string $validation): bool - { - return str_ends_with($validation, "|null"); - } - /** * Splits a string into lines. * @param string $fileContent The string to be split. From 1d1f32cec009f6e9d0bee47e0c55c9ee4f200671 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 14:43:03 +0100 Subject: [PATCH 081/103] replaced comments with doc-comments --- .../AnnotationConversion/NetteAnnotationConverter.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index ad9121f30..8fa3ec500 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -26,11 +26,16 @@ class NetteAnnotationConverter */ private static string $paramRegex = "/\*\s*@Param\((?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?(?:([a-z]+?=.+?),?\s*\*?\s*)?([a-z]+?=.+)\)/"; - // placeholder for detected nette annotations (prefixed with "@Param") - // this text must not be present in the presenter files + /** + * Placeholder for detected nette annotations (prefixed with "@Param"), + * this text must not be present in the presenter files. + * @var string A unique string pattern. + */ public static string $attributePlaceholder = "#nette#"; - // maps @Param validation fields to validator classes + /** + * @var array Maps @Param validation fields to validator classes. + */ private static array $netteValidationToValidatorClassDictionary = [ "email" => VEmail::class, // there is one occurrence of this From 8c7de0e6ed4ea5a067e7f0c56a70b377f812b243 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 14:48:02 +0100 Subject: [PATCH 082/103] replaced comments with doc-comments --- .../StandardAnnotationConverter.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php index 5313d6690..cb340b2fc 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -9,9 +9,13 @@ class StandardAnnotationConverter { - // Metadata about endpoints used to determine what class methods are endpoints and what params - // are path and query. Initialized lazily (it cannot be assigned here because it is not a constant expression). - private static ?array $routesMetadata = null; + /** + * Metadata about endpoints used to determine what class methods are endpoints and what params + * are path and query. Initialized lazily (it cannot be assigned here because it is not a constant expression). + * @var ?array An array of dictionaries with "route", "class", and "method" keys. Each dictionary + * represents an endpoint. + */ + private static array $routesMetadata = null; /** * Converts standard PHP annotations (@param) of a presenter to attributes. From 9f08c53b515468bafcf25085940b2dc44d125dc3 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 14:49:25 +0100 Subject: [PATCH 083/103] added type checking comparison --- .../AnnotationConversion/StandardAnnotationConverter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php index cb340b2fc..9934f7210 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -119,7 +119,7 @@ private static function convertEndpointAnnotations(array $endpoints, array $line $paramLineRegex = "/\\$\\b" . $param->name . "\\b/"; $lineIdx = -1; for ($i = 0; $i < count($annotationLines); $i++) { - if (preg_match($paramLineRegex, $annotationLines[$i]) == 1) { + if (preg_match($paramLineRegex, $annotationLines[$i]) === 1) { $lineIdx = $i; break; } From b2ee348c4ac8e5c3a7830981ae94aa46dbd979f0 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 14:50:05 +0100 Subject: [PATCH 084/103] added type checking comparison --- .../AnnotationConversion/StandardAnnotationConverter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php index 9934f7210..cbfcc7dba 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -136,7 +136,7 @@ private static function convertEndpointAnnotations(array $endpoints, array $line $paramContinuationRegex = "/\h*\*\h+[^@]/"; // find out how long the parameter annotation is for ($i = $lineIdx + 1; $i < count($annotationLines); $i++) { - if (preg_match($paramContinuationRegex, $annotationLines[$i]) == 1) { + if (preg_match($paramContinuationRegex, $annotationLines[$i]) === 1) { $paramAnnotationLength += 1; } else { break; From 9af19f311493d72feb7fa5d2a0f9d44f357f3c5e Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 18:05:57 +0100 Subject: [PATCH 085/103] bugfix: added nullability to field --- .../AnnotationConversion/StandardAnnotationConverter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php index cbfcc7dba..a2aa61e0a 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/StandardAnnotationConverter.php @@ -15,7 +15,7 @@ class StandardAnnotationConverter * @var ?array An array of dictionaries with "route", "class", and "method" keys. Each dictionary * represents an endpoint. */ - private static array $routesMetadata = null; + private static ?array $routesMetadata = null; /** * Converts standard PHP annotations (@param) of a presenter to attributes. From 424a49b2787abcd471a79fff92bb799fd0c6fc5e Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 18:23:12 +0100 Subject: [PATCH 086/103] bugfix: explicitly set nullability of a parameter is now considered as well (no actual behavioral changes, all the changed endpoint parameters were optional so nullable by default, now they are explicitly nullable as well) --- app/V1Module/presenters/AsyncJobsPresenter.php | 2 ++ app/V1Module/presenters/ExercisesPresenter.php | 14 +++++++++++--- app/V1Module/presenters/GroupsPresenter.php | 3 ++- app/V1Module/presenters/PipelinesPresenter.php | 6 ++++-- app/V1Module/presenters/SubmitPresenter.php | 4 ++-- app/V1Module/presenters/UsersPresenter.php | 6 ++++-- .../NetteAnnotationConverter.php | 7 ++++++- 7 files changed, 31 insertions(+), 11 deletions(-) diff --git a/app/V1Module/presenters/AsyncJobsPresenter.php b/app/V1Module/presenters/AsyncJobsPresenter.php index 1aaea3534..5b4602ad9 100644 --- a/app/V1Module/presenters/AsyncJobsPresenter.php +++ b/app/V1Module/presenters/AsyncJobsPresenter.php @@ -104,12 +104,14 @@ public function checkList() new VInt(), "Maximal time since completion (in seconds), null = only pending operations", required: false, + nullable: true, )] #[Query( "includeScheduled", new VBool(), "If true, pending scheduled events will be listed as well", required: false, + nullable: true, )] public function actionList(?int $ageThreshold, ?bool $includeScheduled) { diff --git a/app/V1Module/presenters/ExercisesPresenter.php b/app/V1Module/presenters/ExercisesPresenter.php index 46a3d286c..52aa1c60b 100644 --- a/app/V1Module/presenters/ExercisesPresenter.php +++ b/app/V1Module/presenters/ExercisesPresenter.php @@ -199,19 +199,21 @@ public function checkDefault() * @GET */ #[Query("offset", new VInt(), "Index of the first result.", required: false)] - #[Query("limit", new VInt(), "Maximal number of results returned.", required: false)] + #[Query("limit", new VInt(), "Maximal number of results returned.", required: false, nullable: true)] #[Query( "orderBy", new VString(), "Name of the column (column concept). The '!' prefix indicate descending order.", required: false, + nullable: true, )] - #[Query("filters", new VArray(), "Named filters that prune the result.", required: false)] + #[Query("filters", new VArray(), "Named filters that prune the result.", required: false, nullable: true)] #[Query( "locale", new VString(), "Currently set locale (used to augment order by clause if necessary),", required: false, + nullable: true, )] public function actionDefault( int $offset = 0, @@ -263,7 +265,13 @@ public function checkAuthors() * @GET */ #[Query("instanceId", new VString(), "Id of an instance from which the authors are listed.", required: false)] - #[Query("groupId", new VString(), "A group where the relevant exercises can be seen (assigned).", required: false)] + #[Query( + "groupId", + new VString(), + "A group where the relevant exercises can be seen (assigned).", + required: false, + nullable: true, + )] public function actionAuthors(string $instanceId = null, string $groupId = null) { $authors = $this->exercises->getAuthors($instanceId, $groupId, $this->groups); diff --git a/app/V1Module/presenters/GroupsPresenter.php b/app/V1Module/presenters/GroupsPresenter.php index 1f97226f7..64aad84f6 100644 --- a/app/V1Module/presenters/GroupsPresenter.php +++ b/app/V1Module/presenters/GroupsPresenter.php @@ -184,7 +184,7 @@ class GroupsPresenter extends BasePresenter * Get a list of all non-archived groups a user can see. The return set is filtered by parameters. * @GET */ - #[Query("instanceId", new VString(), "Only groups of this instance are returned.", required: false)] + #[Query("instanceId", new VString(), "Only groups of this instance are returned.", required: false, nullable: true)] #[Query( "ancestors", new VBool(), @@ -196,6 +196,7 @@ class GroupsPresenter extends BasePresenter new VString(), "Search string. Only groups containing this string as a substring of their names are returned.", required: false, + nullable: true, )] #[Query("archived", new VBool(), "Include also archived groups in the result.", required: false)] #[Query( diff --git a/app/V1Module/presenters/PipelinesPresenter.php b/app/V1Module/presenters/PipelinesPresenter.php index 2a6003a3d..c3d22bb0c 100644 --- a/app/V1Module/presenters/PipelinesPresenter.php +++ b/app/V1Module/presenters/PipelinesPresenter.php @@ -156,19 +156,21 @@ public function checkDefault(string $search = null) * @GET */ #[Query("offset", new VInt(), "Index of the first result.", required: false)] - #[Query("limit", new VInt(), "Maximal number of results returned.", required: false)] + #[Query("limit", new VInt(), "Maximal number of results returned.", required: false, nullable: true)] #[Query( "orderBy", new VString(), "Name of the column (column concept). The '!' prefix indicate descending order.", required: false, + nullable: true, )] - #[Query("filters", new VArray(), "Named filters that prune the result.", required: false)] + #[Query("filters", new VArray(), "Named filters that prune the result.", required: false, nullable: true)] #[Query( "locale", new VString(), "Currently set locale (used to augment order by clause if necessary),", required: false, + nullable: true, )] public function actionDefault( int $offset = 0, diff --git a/app/V1Module/presenters/SubmitPresenter.php b/app/V1Module/presenters/SubmitPresenter.php index 20190befd..9e27b82ce 100644 --- a/app/V1Module/presenters/SubmitPresenter.php +++ b/app/V1Module/presenters/SubmitPresenter.php @@ -224,7 +224,7 @@ public function checkCanSubmit(string $id, string $userId = null) * @throws NotFoundException */ #[Path("id", new VString(), "Identifier of the assignment", required: true)] - #[Query("userId", new VString(), "Identification of the user", required: false)] + #[Query("userId", new VString(), "Identification of the user", required: false, nullable: true)] public function actionCanSubmit(string $id, string $userId = null) { $assignment = $this->assignments->findOrThrow($id); @@ -452,7 +452,7 @@ public function checkPreSubmit(string $id, string $userId = null) */ #[Post("files", new VArray())] #[Path("id", new VString(), "identifier of assignment", required: true)] - #[Query("userId", new VString(), "Identifier of the submission author", required: false)] + #[Query("userId", new VString(), "Identifier of the submission author", required: false, nullable: true)] public function actionPreSubmit(string $id, string $userId = null) { $assignment = $this->assignments->findOrThrow($id); diff --git a/app/V1Module/presenters/UsersPresenter.php b/app/V1Module/presenters/UsersPresenter.php index 129296b27..7ace3f00f 100644 --- a/app/V1Module/presenters/UsersPresenter.php +++ b/app/V1Module/presenters/UsersPresenter.php @@ -125,19 +125,21 @@ public function checkDefault() * @GET */ #[Query("offset", new VInt(), "Index of the first result.", required: false)] - #[Query("limit", new VInt(), "Maximal number of results returned.", required: false)] + #[Query("limit", new VInt(), "Maximal number of results returned.", required: false, nullable: true)] #[Query( "orderBy", new VString(), "Name of the column (column concept). The '!' prefix indicate descending order.", required: false, + nullable: true, )] - #[Query("filters", new VArray(), "Named filters that prune the result.", required: false)] + #[Query("filters", new VArray(), "Named filters that prune the result.", required: false, nullable: true)] #[Query( "locale", new VString(), "Currently set locale (used to augment order by clause if necessary),", required: false, + nullable: true, )] public function actionDefault( int $offset = 0, diff --git a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php index 8fa3ec500..313835907 100644 --- a/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php +++ b/app/helpers/MetaFormats/AnnotationConversion/NetteAnnotationConverter.php @@ -231,13 +231,18 @@ public static function convertRegexCapturesToParenthesesBuilder(array $annotatio } $validation = $annotationParameters["validation"]; - // check nullability + // check nullability, it is either in the validation string, or set explicitly // validation strings contain the 'null' qualifier always at the end of the string. $nullabilitySuffix = "|null"; if (str_ends_with($validation, $nullabilitySuffix)) { // remove the '|null' $validation = substr($validation, 0, -strlen($nullabilitySuffix)); $nullable = true; + // check for explicit nullability + } elseif (array_key_exists("nullable", $annotationParameters)) { + // if it is explicitly not nullable but at the same time has to be nullable due to another factor, + // make it nullable (the other factor can be missing validation) + $nullable |= $annotationParameters["nullable"] === "true"; } // this will always produce a single validator (the annotations do not contain multiple validation fields) From 728de688e3b10bfa6786e68b92fc8476d8893c87 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 18:26:55 +0100 Subject: [PATCH 087/103] simplified doc-comment typename --- app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php | 2 +- app/helpers/MetaFormats/Attributes/Param.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php index c4c38fda9..9f34d53c6 100644 --- a/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php +++ b/app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php @@ -20,7 +20,7 @@ class FormatParameterAttribute public bool $nullable; /** - * @param \App\Helpers\MetaFormats\Type $type The request parameter type (Post or Query). + * @param Type $type The request parameter type (Post or Query). * @param mixed $validators A validator object or an array of validators applied to the request parameter. * @param string $description The description of the request parameter. * @param bool $required Whether the request parameter is required. diff --git a/app/helpers/MetaFormats/Attributes/Param.php b/app/helpers/MetaFormats/Attributes/Param.php index f0ef6362b..8e90aba82 100644 --- a/app/helpers/MetaFormats/Attributes/Param.php +++ b/app/helpers/MetaFormats/Attributes/Param.php @@ -14,7 +14,7 @@ class Param extends FormatParameterAttribute public string $paramName; /** - * @param \App\Helpers\MetaFormats\Type $type The request parameter type (Post or Query). + * @param Type $type The request parameter type (Post or Query). * @param string $name The name of the request parameter. * @param mixed $validators A validator object or an array of validators applied to the request parameter. * @param string $description The description of the request parameter. From efa5084c0779b7da52195cba3ffb865f086972e3 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 18:27:42 +0100 Subject: [PATCH 088/103] renamed file to match class name --- .../MetaFormats/Attributes/{FormatAttribute.php => Format.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/helpers/MetaFormats/Attributes/{FormatAttribute.php => Format.php} (100%) diff --git a/app/helpers/MetaFormats/Attributes/FormatAttribute.php b/app/helpers/MetaFormats/Attributes/Format.php similarity index 100% rename from app/helpers/MetaFormats/Attributes/FormatAttribute.php rename to app/helpers/MetaFormats/Attributes/Format.php From b52d924173f636b5f0a42c0a5cdabb04ad4ee32d Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 18:31:32 +0100 Subject: [PATCH 089/103] improved attribute class description --- app/helpers/MetaFormats/Attributes/FPath.php | 2 +- app/helpers/MetaFormats/Attributes/FPost.php | 2 +- app/helpers/MetaFormats/Attributes/FQuery.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/MetaFormats/Attributes/FPath.php b/app/helpers/MetaFormats/Attributes/FPath.php index 3da3899fa..e4a6c4b9a 100644 --- a/app/helpers/MetaFormats/Attributes/FPath.php +++ b/app/helpers/MetaFormats/Attributes/FPath.php @@ -6,7 +6,7 @@ use Attribute; /** - * Attribute used to annotate format definition properties. + * Attribute used to annotate format definition properties representing path parameters. */ #[Attribute(Attribute::TARGET_PROPERTY)] class FPath extends FormatParameterAttribute diff --git a/app/helpers/MetaFormats/Attributes/FPost.php b/app/helpers/MetaFormats/Attributes/FPost.php index 9c61cf91c..55e099cc5 100644 --- a/app/helpers/MetaFormats/Attributes/FPost.php +++ b/app/helpers/MetaFormats/Attributes/FPost.php @@ -6,7 +6,7 @@ use Attribute; /** - * Attribute used to annotate format definition properties. + * Attribute used to annotate format definition properties representing post parameters. */ #[Attribute(Attribute::TARGET_PROPERTY)] class FPost extends FormatParameterAttribute diff --git a/app/helpers/MetaFormats/Attributes/FQuery.php b/app/helpers/MetaFormats/Attributes/FQuery.php index e3ab26c6e..3cf21c029 100644 --- a/app/helpers/MetaFormats/Attributes/FQuery.php +++ b/app/helpers/MetaFormats/Attributes/FQuery.php @@ -6,7 +6,7 @@ use Attribute; /** - * Attribute used to annotate format definition properties. + * Attribute used to annotate format definition properties representing query parameters. */ #[Attribute(Attribute::TARGET_PROPERTY)] class FQuery extends FormatParameterAttribute From 311e9fadd07d8a6f017a0d90c5728586ad8f160b Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 18:34:43 +0100 Subject: [PATCH 090/103] fixed misleading attribute comments --- app/helpers/MetaFormats/Attributes/Param.php | 4 ++-- app/helpers/MetaFormats/Attributes/Path.php | 2 +- app/helpers/MetaFormats/Attributes/Post.php | 2 +- app/helpers/MetaFormats/Attributes/Query.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/helpers/MetaFormats/Attributes/Param.php b/app/helpers/MetaFormats/Attributes/Param.php index 8e90aba82..636370309 100644 --- a/app/helpers/MetaFormats/Attributes/Param.php +++ b/app/helpers/MetaFormats/Attributes/Param.php @@ -6,7 +6,7 @@ use Attribute; /** - * Attribute used to annotate individual post or query parameters of endpoints. + * Attribute used to annotate individual parameters of endpoints. */ #[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)] class Param extends FormatParameterAttribute @@ -14,7 +14,7 @@ class Param extends FormatParameterAttribute public string $paramName; /** - * @param Type $type The request parameter type (Post or Query). + * @param Type $type The request parameter type (Path, Query or Post). * @param string $name The name of the request parameter. * @param mixed $validators A validator object or an array of validators applied to the request parameter. * @param string $description The description of the request parameter. diff --git a/app/helpers/MetaFormats/Attributes/Path.php b/app/helpers/MetaFormats/Attributes/Path.php index 06d4c38fe..7df7cca1e 100644 --- a/app/helpers/MetaFormats/Attributes/Path.php +++ b/app/helpers/MetaFormats/Attributes/Path.php @@ -6,7 +6,7 @@ use Attribute; /** - * Attribute used to annotate individual post or query parameters of endpoints. + * Attribute used to annotate individual path parameters of endpoints. */ #[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)] class Path extends Param diff --git a/app/helpers/MetaFormats/Attributes/Post.php b/app/helpers/MetaFormats/Attributes/Post.php index f57b0771d..0cfacfb96 100644 --- a/app/helpers/MetaFormats/Attributes/Post.php +++ b/app/helpers/MetaFormats/Attributes/Post.php @@ -6,7 +6,7 @@ use Attribute; /** - * Attribute used to annotate individual post or query parameters of endpoints. + * Attribute used to annotate individual post parameters of endpoints. */ #[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)] class Post extends Param diff --git a/app/helpers/MetaFormats/Attributes/Query.php b/app/helpers/MetaFormats/Attributes/Query.php index a229929ab..dc6ea4b90 100644 --- a/app/helpers/MetaFormats/Attributes/Query.php +++ b/app/helpers/MetaFormats/Attributes/Query.php @@ -6,7 +6,7 @@ use Attribute; /** - * Attribute used to annotate individual post or query parameters of endpoints. + * Attribute used to annotate individual query parameters of endpoints. */ #[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)] class Query extends Param From ec70d277efc7d68ed7ee01af7eb280c2ef106450 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 18:53:54 +0100 Subject: [PATCH 091/103] improved FormatCache::formatExists performance, added getter for a hash set of format names --- app/helpers/MetaFormats/FormatCache.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/FormatCache.php b/app/helpers/MetaFormats/FormatCache.php index 6b6bd81cd..54e574475 100644 --- a/app/helpers/MetaFormats/FormatCache.php +++ b/app/helpers/MetaFormats/FormatCache.php @@ -10,7 +10,10 @@ */ class FormatCache { + // do not access the following three arrays directly, use the getter methods instead + // (there is no guarantee that the arrays are initialized) private static ?array $formatNames = null; + private static ?array $formatNamesHashSet = null; private static ?array $formatToFieldFormatsMap = null; /** @@ -40,9 +43,24 @@ public static function getFormatNames(): array return self::$formatNames; } + /** + * @return array Returns a hash set of all defined formats (actually a dictionary with arbitrary values). + */ + public static function getFormatNamesHashSet(): array + { + if (self::$formatNamesHashSet == null) { + $formatNames = self::getFormatNames(); + self::$formatNamesHashSet = []; + foreach ($formatNames as $formatName) { + self::$formatNamesHashSet[$formatName] = true; + } + } + return self::$formatNamesHashSet; + } + public static function formatExists(string $format): bool { - return in_array($format, self::getFormatNames()); + return array_key_exists($format, self::getFormatNamesHashSet()); } /** From 13361ad9e81b92c0b490ee7d0aa450a649a65fe7 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 19:09:16 +0100 Subject: [PATCH 092/103] added doc-comment for UserFormat --- app/helpers/MetaFormats/FormatDefinitions/UserFormat.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php index 20d21cab3..ffb46cf84 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php @@ -11,6 +11,9 @@ use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VString; +/** + * Format definition used by the RegistrationPresenter::actionCreateInvitation endpoint. + */ #[Format(UserFormat::class)] class UserFormat extends MetaFormat { From 7003162f55ffe167119460afff29471852e1601b Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 19:18:45 +0100 Subject: [PATCH 093/103] changed validator --- app/helpers/MetaFormats/FormatDefinitions/UserFormat.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php index ffb46cf84..d400a9097 100644 --- a/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php +++ b/app/helpers/MetaFormats/FormatDefinitions/UserFormat.php @@ -10,6 +10,7 @@ use App\Helpers\MetaFormats\Validators\VArray; use App\Helpers\MetaFormats\Validators\VEmail; use App\Helpers\MetaFormats\Validators\VString; +use App\Helpers\MetaFormats\Validators\VUuid; /** * Format definition used by the RegistrationPresenter::actionCreateInvitation endpoint. @@ -26,7 +27,7 @@ class UserFormat extends MetaFormat #[FPost(new VString(2), "Last name")] public ?string $lastName; - #[FPost(new VString(1), "Identifier of the instance to register in")] + #[FPost(new VUuid(), "Identifier of the instance to register in")] public ?string $instanceId; #[FPost(new VString(1), "Titles that are placed before user name", required: false)] From d3e649fb669814a6acbbb5220771a591aca2229f Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 19:20:34 +0100 Subject: [PATCH 094/103] removed deprecated method --- app/helpers/MetaFormats/MetaFormatHelper.php | 25 -------------------- 1 file changed, 25 deletions(-) diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index eaa88fe48..c53fac5ff 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -22,31 +22,6 @@ class MetaFormatHelper private static string $formatDefinitionFolder = __DIR__ . '/FormatDefinitions'; private static string $formatDefinitionsNamespace = "App\\Helpers\\MetaFormats\\FormatDefinitions"; - /** - * Checks all @checked_param annotations of a method and returns a map from parameter names to their formats. - * @param string $className The name of the containing class. - * @param string $methodName The name of the method. - * @return array - */ - public static function extractMethodCheckedParams(string $className, string $methodName): array - { - $annotations = AnnotationHelper::getMethodAnnotations($className, $methodName); - $filtered = AnnotationHelper::filterAnnotations($annotations, "@checked_param"); - - $formatPrefix = "format:"; - - $paramMap = []; - foreach ($filtered as $annotation) { - // sample: @checked_param format:group group - $tokens = explode(" ", $annotation); - $format = substr($tokens[1], strlen($formatPrefix)); - $name = $tokens[2]; - $paramMap[$name] = $format; - } - - return $paramMap; - } - /** * Checks whether an entity contains a Format attribute and extracts the format if so. * @param \ReflectionClass|\ReflectionProperty|\ReflectionMethod $reflectionObject A reflection From a7b342be1eec98948a320fe67140371ed5b6b735 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 19:44:28 +0100 Subject: [PATCH 095/103] improved VDouble validation --- app/helpers/MetaFormats/Validators/VDouble.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VDouble.php b/app/helpers/MetaFormats/Validators/VDouble.php index d9600c57c..94e530667 100644 --- a/app/helpers/MetaFormats/Validators/VDouble.php +++ b/app/helpers/MetaFormats/Validators/VDouble.php @@ -15,11 +15,11 @@ class VDouble public function validate(mixed $value) { // check if it is a double - if (MetaFormatHelper::checkType($value, PhpTypes::Double)) { + if (is_double($value)) { return true; } - // the value may be a string containing the number + // the value may be a string containing the number, or an integer return is_numeric($value); } } From 29a03e3b4f7572603c70668590d8f469ce295ab9 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 19:49:48 +0100 Subject: [PATCH 096/103] improved VInt validation --- app/helpers/MetaFormats/Validators/VInt.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VInt.php b/app/helpers/MetaFormats/Validators/VInt.php index 7d5f08752..7fa336ff4 100644 --- a/app/helpers/MetaFormats/Validators/VInt.php +++ b/app/helpers/MetaFormats/Validators/VInt.php @@ -19,8 +19,8 @@ public function getExampleValue() public function validate(mixed $value) { - // check if it is an integer - if (MetaFormatHelper::checkType($value, PhpTypes::Int)) { + // check if it is an integer (does not handle integer strings) + if (is_int($value)) { return true; } @@ -29,6 +29,7 @@ public function validate(mixed $value) return false; } + // if it is a numeric string, check if it is an integer or float return intval($value) == floatval($value); } } From 14b11df5ea656674cce704c75daaeb7dbc1e5d13 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 27 Feb 2025 19:51:19 +0100 Subject: [PATCH 097/103] improved VString validation --- app/helpers/MetaFormats/Validators/VString.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/MetaFormats/Validators/VString.php b/app/helpers/MetaFormats/Validators/VString.php index 669ed4602..fc02c48f7 100644 --- a/app/helpers/MetaFormats/Validators/VString.php +++ b/app/helpers/MetaFormats/Validators/VString.php @@ -37,7 +37,7 @@ public function getExampleValue() public function validate(mixed $value): bool { // do not allow other types - if (!MetaFormatHelper::checkType($value, PhpTypes::String)) { + if (!is_string($value)) { return false; } From c0335c5d7fb3a5ee5439d79bc734063d20f701f4 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 28 Feb 2025 12:48:04 +0100 Subject: [PATCH 098/103] made VBool stricter --- app/helpers/MetaFormats/Validators/VBool.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VBool.php b/app/helpers/MetaFormats/Validators/VBool.php index 00dcdae4c..3d9ece054 100644 --- a/app/helpers/MetaFormats/Validators/VBool.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -6,7 +6,7 @@ use App\Helpers\MetaFormats\PhpTypes; /** - * Validates boolean values. Accepts bools, "true", "false", 0 and 1. + * Validates boolean values. Accepts only boolean true and false. */ class VBool { @@ -14,11 +14,6 @@ class VBool public function validate(mixed $value) { - // support stringified values as well as 0 and 1 - return MetaFormatHelper::checkType($value, PhpTypes::Bool) - || $value == 0 - || $value == 1 - || $value == "true" - || $value == "false"; + return $value === true || $value === false; } } From 74c55a788ee8439646d61fd4951f0496257e2f07 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 28 Feb 2025 12:49:30 +0100 Subject: [PATCH 099/103] removed gettype type validation --- app/helpers/MetaFormats/MetaFormatHelper.php | 11 ----------- app/helpers/MetaFormats/PhpTypes.php | 16 ---------------- app/helpers/MetaFormats/Validators/VBool.php | 3 --- app/helpers/MetaFormats/Validators/VDouble.php | 3 --- app/helpers/MetaFormats/Validators/VInt.php | 5 +---- app/helpers/MetaFormats/Validators/VString.php | 3 --- 6 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 app/helpers/MetaFormats/PhpTypes.php diff --git a/app/helpers/MetaFormats/MetaFormatHelper.php b/app/helpers/MetaFormats/MetaFormatHelper.php index c53fac5ff..84b4d98ce 100644 --- a/app/helpers/MetaFormats/MetaFormatHelper.php +++ b/app/helpers/MetaFormats/MetaFormatHelper.php @@ -189,15 +189,4 @@ public static function createFormatInstance(string $format): MetaFormat $instance = new $format(); return $instance; } - - /** - * Checks whether a value is of a given type. - * @param mixed $value The value to be tested. - * @param \App\Helpers\MetaFormats\PhpTypes $type The desired type of the value. - * @return bool Returns whether the value is of the given type. - */ - public static function checkType($value, PhpTypes $type): bool - { - return gettype($value) === $type->value; - } } diff --git a/app/helpers/MetaFormats/PhpTypes.php b/app/helpers/MetaFormats/PhpTypes.php deleted file mode 100644 index 22b1105f4..000000000 --- a/app/helpers/MetaFormats/PhpTypes.php +++ /dev/null @@ -1,16 +0,0 @@ - Date: Fri, 28 Feb 2025 12:57:06 +0100 Subject: [PATCH 100/103] tests now send proper booleans to endpoints instead of strings and ints --- tests/Presenters/CommentsPresenter.phpt | 10 +++++----- tests/Presenters/ExercisesPresenter.phpt | 6 +++--- tests/Presenters/InstancesPresenter.phpt | 8 ++++---- tests/Presenters/UsersPresenter.phpt | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/Presenters/CommentsPresenter.phpt b/tests/Presenters/CommentsPresenter.phpt index 9abd855d3..e9dbe3d08 100644 --- a/tests/Presenters/CommentsPresenter.phpt +++ b/tests/Presenters/CommentsPresenter.phpt @@ -110,7 +110,7 @@ class TestCommentsPresenter extends Tester\TestCase 'V1:Comments', 'POST', ['action' => 'addComment', 'id' => '6b89a6df-f7e8-4c2c-a216-1b7cb4391647'], // mainThread - ['text' => 'some comment text', 'isPrivate' => 'false'] + ['text' => 'some comment text', 'isPrivate' => false] ); $response = $this->presenter->run($request); Assert::type(Nette\Application\Responses\JsonResponse::class, $response); @@ -141,7 +141,7 @@ class TestCommentsPresenter extends Tester\TestCase 'V1:Comments', 'POST', ['action' => 'addComment', 'id' => $assignmentSolution->getId()], - ['text' => 'some comment text', 'isPrivate' => 'false'] + ['text' => 'some comment text', 'isPrivate' => false] ); $response = $this->presenter->run($request); Assert::type(Nette\Application\Responses\JsonResponse::class, $response); @@ -172,7 +172,7 @@ class TestCommentsPresenter extends Tester\TestCase 'V1:Comments', 'POST', ['action' => 'addComment', 'id' => $referenceSolution->getId()], - ['text' => 'some comment text', 'isPrivate' => 'false'] + ['text' => 'some comment text', 'isPrivate' => false] ); $response = $this->presenter->run($request); Assert::type(Nette\Application\Responses\JsonResponse::class, $response); @@ -202,7 +202,7 @@ class TestCommentsPresenter extends Tester\TestCase 'V1:Comments', 'POST', ['action' => 'addComment', 'id' => $assignment->getId()], - ['text' => 'some comment text', 'isPrivate' => 'false'] + ['text' => 'some comment text', 'isPrivate' => false] ); $response = $this->presenter->run($request); Assert::type(Nette\Application\Responses\JsonResponse::class, $response); @@ -226,7 +226,7 @@ class TestCommentsPresenter extends Tester\TestCase 'V1:Comments', 'POST', ['action' => 'addComment', 'id' => '5d45dcd0-50e7-4b2a-a291-cfe4b5fb5cbb'], // dummy thread (nonexist) - ['text' => 'some comment text', 'isPrivate' => 'false'] + ['text' => 'some comment text', 'isPrivate' => false] ); $response = $this->presenter->run($request); Assert::type(Nette\Application\Responses\JsonResponse::class, $response); diff --git a/tests/Presenters/ExercisesPresenter.phpt b/tests/Presenters/ExercisesPresenter.phpt index 5ef1f6296..3da90ca52 100644 --- a/tests/Presenters/ExercisesPresenter.phpt +++ b/tests/Presenters/ExercisesPresenter.phpt @@ -979,7 +979,7 @@ class TestExercisesPresenter extends Tester\TestCase 'V1:Exercises', 'POST', ['action' => 'setArchived', 'id' => $exercise->getId()], - ['archived' => 'true'] + ['archived' => true] ); $this->presenter->exercises->refresh($exercise); @@ -1002,7 +1002,7 @@ class TestExercisesPresenter extends Tester\TestCase 'V1:Exercises', 'POST', ['action' => 'setArchived', 'id' => $exercise->getId()], - ['archived' => 'true'] + ['archived' => true] ); }, ForbiddenRequestException::class @@ -1022,7 +1022,7 @@ class TestExercisesPresenter extends Tester\TestCase 'V1:Exercises', 'POST', ['action' => 'setArchived', 'id' => $exercise->getId()], - ['archived' => 'false'] + ['archived' => false] ); $this->presenter->exercises->refresh($exercise); diff --git a/tests/Presenters/InstancesPresenter.phpt b/tests/Presenters/InstancesPresenter.phpt index 28edbb0d1..000bd3ae2 100644 --- a/tests/Presenters/InstancesPresenter.phpt +++ b/tests/Presenters/InstancesPresenter.phpt @@ -97,7 +97,7 @@ class TestInstancesPresenter extends Tester\TestCase 'V1:Instances', 'POST', ['action' => 'createInstance'], - ['name' => 'NIOT', 'description' => 'Just a new instance', 'isOpen' => 'true'] + ['name' => 'NIOT', 'description' => 'Just a new instance', 'isOpen' => true] ); $response = $this->presenter->run($request); Assert::type(Nette\Application\Responses\JsonResponse::class, $response); @@ -121,7 +121,7 @@ class TestInstancesPresenter extends Tester\TestCase 'V1:Instances', 'POST', ['action' => 'updateInstance', 'id' => $instance->getId()], - ['isOpen' => 'false'] + ['isOpen' => false] ); $response = $this->presenter->run($request); Assert::type(Nette\Application\Responses\JsonResponse::class, $response); @@ -143,7 +143,7 @@ class TestInstancesPresenter extends Tester\TestCase 'V1:Instances', 'POST', ['action' => 'createInstance'], - ['name' => 'NIOT', 'description' => 'Just a new instance', 'isOpen' => 'true'] + ['name' => 'NIOT', 'description' => 'Just a new instance', 'isOpen' => true] ); $response = $this->presenter->run($request); $newInstanceId = $response->getPayload()['payload']['id']; @@ -225,7 +225,7 @@ class TestInstancesPresenter extends Tester\TestCase 'V1:Instances', 'POST', ['action' => 'updateLicence', 'licenceId' => $newLicence->getId()], - ['note' => 'Changed description', 'validUntil' => '2020-01-01 13:02:56', 'isValid' => 'false'] + ['note' => 'Changed description', 'validUntil' => '2020-01-01 13:02:56', 'isValid' => false] ); $response = $this->presenter->run($request); Assert::type(Nette\Application\Responses\JsonResponse::class, $response); diff --git a/tests/Presenters/UsersPresenter.phpt b/tests/Presenters/UsersPresenter.phpt index 26f581228..1b9d7467a 100644 --- a/tests/Presenters/UsersPresenter.phpt +++ b/tests/Presenters/UsersPresenter.phpt @@ -830,7 +830,7 @@ class TestUsersPresenter extends Tester\TestCase $this->presenterPath, 'POST', ['action' => 'setAllowed', 'id' => $user->getId()], - ['isAllowed' => 0] + ['isAllowed' => false] ); Assert::same($user->getId(), $payload['id']); From a7b607f5e8fc58a6202096edd6680cc3b7e9628f Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Fri, 28 Feb 2025 13:04:48 +0100 Subject: [PATCH 101/103] VBool now supports 'false' for one test to pass --- app/helpers/MetaFormats/Validators/VBool.php | 2 +- tests/Presenters/InstancesPresenter.phpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/MetaFormats/Validators/VBool.php b/app/helpers/MetaFormats/Validators/VBool.php index 5c3964954..ec1b0e42a 100644 --- a/app/helpers/MetaFormats/Validators/VBool.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -11,6 +11,6 @@ class VBool public function validate(mixed $value) { - return $value === true || $value === false; + return $value === true || $value === false || $value === 'false'; } } diff --git a/tests/Presenters/InstancesPresenter.phpt b/tests/Presenters/InstancesPresenter.phpt index 000bd3ae2..68b8e585c 100644 --- a/tests/Presenters/InstancesPresenter.phpt +++ b/tests/Presenters/InstancesPresenter.phpt @@ -121,7 +121,7 @@ class TestInstancesPresenter extends Tester\TestCase 'V1:Instances', 'POST', ['action' => 'updateInstance', 'id' => $instance->getId()], - ['isOpen' => false] + ['isOpen' => 'false'] ); $response = $this->presenter->run($request); Assert::type(Nette\Application\Responses\JsonResponse::class, $response); From 311f5671c0e4f99b87d94cb95dc564ec2ce7a080 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Sat, 1 Mar 2025 11:46:45 +0100 Subject: [PATCH 102/103] added a todo regarding VBool --- app/helpers/MetaFormats/Validators/VBool.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/MetaFormats/Validators/VBool.php b/app/helpers/MetaFormats/Validators/VBool.php index ec1b0e42a..55064756e 100644 --- a/app/helpers/MetaFormats/Validators/VBool.php +++ b/app/helpers/MetaFormats/Validators/VBool.php @@ -11,6 +11,7 @@ class VBool public function validate(mixed $value) { + ///TODO: remove 'false' once the testUpdateInstance test issue is fixed. return $value === true || $value === false || $value === 'false'; } } From 687e7e1bdbc7584dbcea78997841de69ee2f6f43 Mon Sep 17 00:00:00 2001 From: Vojtech Kloda Date: Thu, 6 Mar 2025 12:02:26 +0100 Subject: [PATCH 103/103] bugfix: removed erroneous query param --- app/V1Module/presenters/ExercisesPresenter.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/V1Module/presenters/ExercisesPresenter.php b/app/V1Module/presenters/ExercisesPresenter.php index 52aa1c60b..82e58bdf7 100644 --- a/app/V1Module/presenters/ExercisesPresenter.php +++ b/app/V1Module/presenters/ExercisesPresenter.php @@ -873,9 +873,8 @@ public function checkAddTag(string $id) * @throws ForbiddenRequestException * @throws InvalidArgumentException */ - #[Query("name", new VString(1, 32), "Name of the newly added tag to given exercise")] #[Path("id", new VString(), required: true)] - #[Path("name", new VString(), required: true)] + #[Path("name", new VString(1, 32), "Name of the newly added tag to given exercise", required: true)] public function actionAddTag(string $id, string $name) { if (!$this->exerciseTags->verifyTagName($name)) {