initial commit

This commit is contained in:
Victor Westerlund 2023-11-17 12:21:13 +01:00
commit efc908b31b
6 changed files with 380 additions and 0 deletions

48
.gitignore vendored Normal file
View file

@ -0,0 +1,48 @@
# Bootstrapping #
#################
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
.directory
# Tool specific files #
#######################
# vim
*~
*.swp
*.swo
# sublime text & textmate
*.sublime-*
*.stTheme.cache
*.tmlanguage.cache
*.tmPreferences.cache
# Eclipse
.settings/*
# JetBrains, aka PHPStorm, IntelliJ IDEA
.idea/*
# NetBeans
nbproject/*
# Visual Studio Code
.vscode
# Sass preprocessor
.sass-cache/

0
README.md Normal file
View file

21
composer.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "reflect/plugin-rules",
"description": "Add request search paramter and request body constraints to an API built with Reflect",
"type": "library",
"license": "GPL-3.0-only",
"authors": [
{
"name": "Victor Westerlund",
"email": "victor.vesterlund@gmail.com"
}
],
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"ReflectRules\\": "src/"
}
},
"require": {
"victorwesterlund/xenum": "dev-master"
}
}

59
composer.lock generated Normal file
View file

@ -0,0 +1,59 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "035091a14ba6701d664e69d17b3f630b",
"packages": [
{
"name": "victorwesterlund/xenum",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/VictorWesterlund/php-xenum.git",
"reference": "99b784841ee5b69fdfcc4c466ef54f3af4ea4a85"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/VictorWesterlund/php-xenum/zipball/99b784841ee5b69fdfcc4c466ef54f3af4ea4a85",
"reference": "99b784841ee5b69fdfcc4c466ef54f3af4ea4a85",
"shasum": ""
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"victorwesterlund\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-3.0-only"
],
"authors": [
{
"name": "Victor Westerlund",
"email": "victor.vesterlund@gmail.com"
}
],
"description": "PHP eXtended Enums. The missing quality-of-life features from PHP 8+ Enums",
"support": {
"issues": "https://github.com/VictorWesterlund/php-xenum/issues",
"source": "https://github.com/VictorWesterlund/php-xenum/tree/1.1.0"
},
"time": "2023-10-09T11:32:07+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"victorwesterlund/xenum": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.0.0"
}

123
src/Rules.php Normal file
View file

@ -0,0 +1,123 @@
<?php
namespace ReflectRules;
use \victorwesterlund\xEnum;
// Supported types for is_type()
enum Type {
use xEnum;
case NUMBER;
case STRING;
case BOOLEAN;
case ARRAY;
case OBJECT;
case NULL;
}
class Rules {
private string $property;
public bool $required = false;
public ?Type $type = null;
public ?int $min = null;
public ?int $max = null;
public function __construct(string $property) {
$this->property = $property;
}
public function get_property_name(): string {
return $this->property;
}
/*
# Constraints
Chain these methods to create rules for a particular property.
When all rules are defiend, the eval_* methods will be called
*/
// A sequential array of additional Rule instances for a
private function object_rules(array $rules): self {
$this->object_rules = $rules;
return $this;
}
// Set the minimum lenth/size for property
public function min(?int $value = null) {
$this->min = $value;
return $this;
}
// Set the maximum length/size for property
public function max(?int $value = null) {
$this->max = $value;
return $this;
}
// This property has to exist in scope
public function required(bool $flag = true): self {
$this->required = $flag;
return $this;
}
// Set property Type
public function type(Type|string $type): self {
// Coerce string to Type enum
if (!($type instanceof Type)) {
$type = Type::fromName($string);
}
$this->type = $type;
return $this;
}
/*
# Eval methods
These methods are used to check conformity against set rules.
Methods are not called until all rules have been defined.
*/
public function eval_required(array $scope): bool {
return array_key_exists($this->property, $scope);
}
public function eval_type(mixed $value): bool {
return match($this->type) {
Type::NUMBER => is_numeric($value),
Type::STRING => is_string($value),
Type::BOOLEAN => is_bool($value),
Type::ARRAY => is_array($value),
Type::OBJECT => $this->eval_object($value),
Type::NULL => is_null($value),
default => true
};
}
public function eval_min(mixed $value): bool {
return match($this->type) {
Type::NUMBER => $this->eval_type($value) && $value >= $this->min,
Type::STRING => $this->eval_type($value) && strlen($value) >= $this->min,
Type::ARRAY,
Type::OBJECT => $this->eval_type($value) && count($value) >= $this->min,
default => true
};
}
public function eval_max(mixed $value): bool {
return match($this->type) {
Type::NUMBER => $this->eval_type($value) && $value <= $this->max,
Type::STRING => $this->eval_type($value) && strlen($value) <= $this->max,
Type::ARRAY,
Type::OBJECT => $this->eval_type($value) && count($value) <= $this->max,
default => true
};
}
// TODO: Recursive Rules eval of multidimensional object
public function eval_object(mixed $object): bool {
return is_array($object);
}
}

129
src/Ruleset.php Normal file
View file

@ -0,0 +1,129 @@
<?php
namespace ReflectRules;
// Use the Response class from Reflect to override endpoint processing if requested
use \Reflect\Response;
use \ReflectRules\Rules;
require_once "../vendor/autoload.php";
require_once "Rules.php";
class Ruleset {
// This plugin will return exit with a Reflect\Response if errors are found
private bool $exit_on_errors;
// Array of RuleError instances
private array $errors = [];
public function __construct(bool $exit_on_errors = true) {
// Set exit on errors override flag
$this->exit_on_errors = $exit_on_errors;
}
// Return property names for all Rules in array
private static function get_property_names(array $rules): array {
return array_map(fn(Rules $rule): string => $rule->get_property_name(), $rules);
}
// ----
// Append an error to the array of errors
private function add_error(string $name, string $message): self {
// Create sub array for name if it doesn't exist
if (!array_key_exists($name, $this->errors)) {
$this->errors[$name] = [];
}
$this->errors[$name][] = $message;
return $this;
}
private function eval_property_name_diff($rules, $scope_keys): void {
// Get property names that aren't defiend in the ruleset
$invalid_properties = array_diff($scope_keys, self::get_property_names($rules));
// Add error for each invalid property name
foreach ($invalid_properties as $invalid_property) {
$this->add_error($invalid_property, "Unknown property name '{$invalid_property}'");
}
}
// Evaluate Rules against a given value
private function eval_rules(Rules $rules, array &$scope): void {
$name = $rules->get_property_name();
// Check if property name exists in scope
if (!$rules->eval_required($scope)) {
// Set property name value on superglobal to null if not defined
$scope[$name] = null;
// Don't perform further processing if the property is optional and not provided
if (!$rules->required) {
return;
}
$this->add_error($name, "Value can not be empty");
}
$value = $scope[$name];
/*
Eval each rule that has been set.
The error messages will be returned
*/
if ($rules->type && !$rules->eval_type($value)) {
$this->add_error($name, "Value must be of type '{$rules->type->name}'");
}
if ($rules->min && !$rules->eval_min($value)) {
$this->add_error($name, "Value must be larger or equal to {$rules->min}");
}
if ($rules->max && !$rules->eval_max($value)) {
$this->add_error($name, "Value must be smaller or equal to {$rules->max}");
}
}
// Evaluate all Rules in this Ruleset against values in scope and return true if no errors were found
private function eval_all_rules(array $rules, array &$scope): bool {
foreach ($rules as $rule) {
$this->eval_rules($rule, $scope);
}
return empty($this->errors);
}
// ----
// Perform request processing on GET properties (search parameters)
public function GET(array $rules): true|array|Response {
$this->eval_property_name_diff($rules, array_keys($_GET));
$is_valid = $this->eval_all_rules($rules, $_GET);
// Return errors as a Reflect\Response
if (!$is_valid && $this->exit_on_errors) {
return new Response($this->errors, 422);
}
return $is_valid ? true : $this->errors;
}
// Perform request processing on POST properties (request body)
public function POST(array $rules): true|array|Response {
$this->eval_property_name_diff($rules, array_keys($_POST));
$is_valid = $this->eval_all_rules($rules, $_POST);
// Return errors as a Reflect\Response
if (!$is_valid && $this->exit_on_errors) {
return new Response($this->errors, 422);
}
return $is_valid ? true : $this->errors;
}
}