Browse Source

Release v2.0.0 - HTTP validation, i18n, and namespace change

phpdevcommunity 3 ngày trước cách đây
mục cha
commit
3fb88217a3

+ 186 - 64
README.md

@@ -8,6 +8,9 @@ Simplify your request processing with `php-requestkit`. This library allows you
 
 * **Schema-based validation:** Define clear and concise validation rules for your request data.
 * **Data transformation:**  Automatically transform and sanitize input data based on your schema.
+* **HTTP Header Validation:** Define rules to validate incoming request headers.
+* **Form & CSRF Processing:** Securely process form submissions with built-in CSRF token validation.
+* **Internationalization (i18n):** Error messages can be easily translated. Comes with English and French built-in.
 * **Multiple data types:** Supports strings, integers, booleans, dates, date-times, and numeric types with various constraints.
 * **Nested data and collections:** Validate complex data structures, including nested objects and arrays.
 * **Error handling:** Provides detailed error messages for easy debugging and user feedback.
@@ -16,7 +19,7 @@ Simplify your request processing with `php-requestkit`. This library allows you
 ## Installation
 
 ```bash
-composer require phpdevcommunity/php-requestkit
+composer require depo/requestkit
 ```
 
 ## Basic Usage
@@ -26,8 +29,8 @@ composer require phpdevcommunity/php-requestkit
 ```php
 <?php
 
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\Type;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Type;
 
 $userSchema = Schema::create([
     'username' => Type::string()->length(5, 20)->required(),
@@ -41,7 +44,7 @@ $userSchema = Schema::create([
 ```php
 <?php
 
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Exceptions\InvalidDataException;
 
 // ... (Schema creation from step 1) ...
 
@@ -80,9 +83,9 @@ This example demonstrates validating data from a REST API endpoint (e.g., POST,
 
 namespace MonApi\Controller;
 
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\Type;
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Type;
+use Depo\RequestKit\Exceptions\InvalidDataException;
 
 class UserController
 {
@@ -156,9 +159,9 @@ Validate parameters passed in the URL's query string.
 
 namespace MonApi\Controller;
 
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\Type;
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Type;
+use Depo\RequestKit\Exceptions\InvalidDataException;
 
 class ProductController
 {
@@ -203,9 +206,9 @@ Validate arrays of data, especially useful for batch operations or list endpoint
 
 namespace MonApi\Controller;
 
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\Type;
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Type;
+use Depo\RequestKit\Exceptions\InvalidDataException;
 
 class OrderController
 {
@@ -249,84 +252,135 @@ $result = $controller->createOrders($ordersData);
 print_r($result); // Will print error array if validation fails
 ```
 
-## Error Handling with `InvalidDataException`
+## Advanced Usage: HTTP Requests, Headers, and Forms
 
-When validation fails, the `Schema::process()` method throws an `InvalidDataException`.  This exception provides methods to access detailed error information.
+While `process()` is great for arrays, the library shines when working with PSR-7 `ServerRequestInterface` objects, allowing you to validate headers, form data, and CSRF tokens seamlessly.
 
-### Retrieving All Errors
+### Validating Request Headers with `withHeaders()`
 
-Use `getErrors()` to get an associative array where keys are field paths and values are error messages.
+You can enforce rules on incoming HTTP headers by chaining the `withHeaders()` method to your schema. This is perfect for validating API keys, content types, or custom headers.
 
 ```php
 <?php
-// ... inside a catch block for InvalidDataException ...
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Type;
+use Psr\Http\Message\ServerRequestInterface;
 
-    } catch (InvalidDataException $e) {
-        $errors = $e->getErrors();
-        // $errors will be like:
-        // [
-        //    'orders.2.product_id' => 'Value must be an integer, got: string',
-        //    'orders.2.quantity' => 'quantity must be at least 1',
-        // ]
-        return ['errors' => $errors];
-    }
+// Assume $request is a PSR-7 ServerRequestInterface object from your framework
+
+$schema = Schema::create([
+    'name' => Type::string()->required(),
+])->withHeaders([
+    'Content-Type' => Type::string()->equals('application/json'),
+    'X-Api-Key' => Type::string()->required()->length(16),
+]);
+
+try {
+    // processHttpRequest validates both headers and the request body
+    $validatedData = $schema->processHttpRequest($request);
+    $name = $validatedData->get('name');
+    
+} catch (InvalidDataException $e) {
+    // Throws an exception if headers or body are invalid
+    // e.g., if 'X-Api-Key' is missing or 'Content-Type' is not 'application/json'
+    http_response_code(400);
+    return ['errors' => $e->getErrors()];
+}
 ```
 
-### Retrieving a Specific Error
+### Processing Forms with CSRF Protection using `processForm()`
 
-Use `getError(string $key)` to get the error message for a specific field path. Returns `null` if no error exists for that path.
+Securely handle form submissions (`application/x-www-form-urlencoded`) with optional CSRF token validation.
+
+**1. Form with CSRF Validation (Recommended)**
+
+Pass the expected CSRF token (e.g., from the user's session) as the second argument. The library will ensure the token is present and matches.
 
 ```php
 <?php
-// ... inside a catch block for InvalidDataException ...
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Type;
+use Psr\Http\Message\ServerRequestInterface;
 
-    } catch (InvalidDataException $e) {
-        $productIdError = $e->getError('orders.2.product_id');
-        if ($productIdError) {
-            // $productIdError will be: 'Value must be an integer, got: string'
-            return ['errors' => ['product_id_error' => $productIdError]]; // Structure error response as needed
-        }
+// Assume $request is a PSR-7 ServerRequestInterface object
+// and $_SESSION['csrf_token'] holds the expected token.
+
+$schema = Schema::create([
+    'username' => Type::string()->required(),
+    'comment' => Type::string()->required(),
+]);
+
+$expectedToken = $_SESSION['csrf_token'] ?? null;
+
+try {
+    // processForm validates the form body and the CSRF token
+    // The third argument '_csrf' is the name of the form field containing the token.
+    $validatedData = $schema->processFormHttpRequest($request, $expectedToken, '_csrf');
+    // The '_csrf' field is automatically removed from the validated data.
+    
+} catch (InvalidDataException $e) {
+    // Throws an exception if form data is invalid or CSRF token is missing/incorrect
+    http_response_code(400);
+    if ($e->getMessage() === 'Invalid CSRF token.') {
+        http_response_code(403); // Forbidden
     }
+    return ['errors' => $e->getErrors()];
+}
 ```
 
-### Formatting Error Response with `toResponse()`
+**2. Form without CSRF Validation**
 
-Use `toResponse()` to get a pre-formatted associative array suitable for returning as an API error response. This includes status, a general error message, and detailed validation errors.
+If you don't need CSRF protection for a specific form (e.g., a public search form), simply omit the second argument (or pass `null`).
+
+```php
+<?php
+// ...
+try {
+    // No CSRF token is expected or validated
+    $validatedData = $schema->processFormHttpRequest($request);
+    
+} catch (InvalidDataException $e) {
+    // ...
+}
+```
+
+## Error Handling with `InvalidDataException`
+
+When validation fails, an `InvalidDataException` is thrown. This exception provides methods to access detailed error information.
+
+### Retrieving All Errors
+
+Use `getErrors()` to get an associative array where keys are field paths and values are error messages.
 
 ```php
 <?php
 // ... inside a catch block for InvalidDataException ...
 
     } catch (InvalidDataException $e) {
-        $response = $e->toResponse();
-        // $response will be like:
+        $errors = $e->getErrors();
+        // $errors will be like:
         // [
-        //     'status' => 'error',
-        //     'message' => 'Validation failed',
-        //     'errors' => [ /* ... detailed errors from getErrors() ... */ ],
+        //    'orders.2.product_id' => 'Value must be an integer, got: string',
+        //    'orders.2.quantity' => 'quantity must be at least 1',
         // ]
-        http_response_code($response['code']); // Set appropriate HTTP status code (e.g., 400)
-        return $response;
+        return ['errors' => $errors];
     }
 ```
 
-### Accessing Exception `message` and `code`
+### Retrieving a Specific Error
 
-`InvalidDataException` extends PHP's base `\Exception`, allowing access to standard exception properties.
+Use `getError(string $key)` to get the error message for a specific field path. Returns `null` if no error exists for that path.
 
 ```php
 <?php
 // ... inside a catch block for InvalidDataException ...
 
     } catch (InvalidDataException $e) {
-        $message = $e->getMessage(); // General error message (e.g., "Validation failed")
-        $code = $e->getCode();       //  Error code (you can customize this)
-
-        return [
-            'message' => $message,
-            'code' => $code,
-            'errors' => $e->getErrors(), // Detailed errors
-        ];
+        $productIdError = $e->getError('orders.2.product_id');
+        if ($productIdError) {
+            // $productIdError will be: 'Value must be an integer, got: string'
+            return ['errors' => ['product_id_error' => $productIdError]]; // Structure error response as needed
+        }
     }
 ```
 
@@ -334,9 +388,12 @@ Use `toResponse()` to get a pre-formatted associative array suitable for returni
 
 `php-requestkit` provides a variety of built-in data types with a rich set of validation rules.
 
-*   **`Type::string()`:**
+*   **General Rules (Available for most types):**
     *   `required()`: Field is mandatory.
     *   `optional()`: Field is optional.
+    *   `strict()`: Strict type validation (e.g., `Type::int()->strict()` will not accept `"123"`).
+
+*   **`Type::string()`:**
     *   `length(min, max)`:  String length constraints.
     *   `trim()`: Trim whitespace from both ends.
     *   `lowercase()`: Convert to lowercase.
@@ -346,35 +403,100 @@ Use `toResponse()` to get a pre-formatted associative array suitable for returni
     *   `removeSpaces()`: Remove all spaces.
     *   `padLeft(length, char)`: Pad string to the left with a character.
     *   `removeChars(...chars)`: Remove specific characters.
-    *   `strict()`: Strict type validation (only accepts strings).
+    *   `equals(value)`: The final value must be strictly equal to the given `value`.
 
-*   **`Type::int()`:**
-    *   `required()`, `optional()`, `strict()`: Same as StringType.
+*   **`Type::int()` & `Type::float()`:**
     *   `min(value)`: Minimum value.
     *   `max(value)`: Maximum value.
+    *   `equals(value)`: The final value must be strictly equal to the given `value`.
+
 
 *   **`Type::bool()`:**
-    *   `required()`, `optional()`, `strict()`: Same as StringType.
+    *   Accepts `true`, `false`, `'true'`, `'false'`, `1`, `0`, `'1'`, `'0'`.
+    *   `equals(value)`: The final value must be strictly equal to the given `value`.
 
 *   **`Type::date()` and `Type::datetime()`:**
-    *   `required()`, `optional()`: Same as StringType.
     *   `format(format)`: Specify the date/datetime format (using PHP date formats).
 
 *   **`Type::numeric()`:**
-    *   `required()`, `optional()`: Same as StringType.  Validates any numeric value (integer or float).
+    *   Validates any numeric value (integer or float).
+    *   `equals(value)`: The final value must be strictly equal to the given `value`.
 
 *   **`Type::item(array $schema)`:** For nested objects/items. Defines a schema for a nested object within the main schema.
 
 *   **`Type::arrayOf(Type $type)`:** For collections/arrays.  Defines that a field should be an array of items, each validated against the provided `Type`.
 *   **`Type::map(Type $type)`:** For key-value objects (associative arrays). Defines that a field should be an object where each value is validated against the provided Type, and keys must be strings.
+
+## Internationalization (i18n)
+
+All error messages are translatable. The library includes English (`en`) and French (`fr`) by default.
+
+### Changing the Language
+
+To switch the language for all subsequent error messages, use the static method `Locale::setLocale()` at the beginning of your application.
+
+```php
+use Depo\RequestKit\Locale;
+
+// Set the active language to French
+Locale::setLocale('fr');
+```
+
+### Adding a New Language
+
+You can easily add support for a new language using `Locale::addMessages()`. Provide the two-letter language code and an associative array of translations. If a key is missing for the active language, the library will automatically fall back to the English version.
+
+Here is a full template for creating a new translation. You only need to translate the values.
+
+```php
+use Depo\RequestKit\Locale;
+
+Locale::addMessages('en', [ // Example for English
+    'error' => [
+        'required' => 'Value is required, but got null or empty string.',
+        'equals' => 'The value does not match the expected value.',
+        'csrf' => 'Invalid CSRF token.',
+        'json' => 'Invalid JSON input: {error}',
+        'type' => [
+            'string' => 'Value must be a string, got: {type}.',
+            'int' => 'Value must be an integer, got: {type}.',
+            'float' => 'Value must be a float, got: {type}.',
+            'bool' => 'Value must be a boolean, got: {type}.',
+            'numeric' => 'Value must be numeric, got: {type}.',
+            'date' => 'Value must be a valid date.',
+            'datetime' => 'Value must be a valid datetime.',
+            'array' => 'Value must be an array.',
+        ],
+        'string' => [
+            'min_length' => 'Value must be at least {min} characters long.',
+            'max_length' => 'Value cannot be longer than {max} characters.',
+            'email' => 'Value must be a valid email address.',
+            'allowed' => 'Value is not allowed.',
+        ],
+        'int' => [
+            'min' => 'Value must be at least {min}.',
+            'max' => 'Value must be no more than {max}.',
+        ],
+        'array' => [
+            'min_items' => 'Value must have at least {min} item(s).',
+            'max_items' => 'Value must have at most {max} item(s).',
+            'integer_keys' => 'All keys must be integers.',
+        ],
+        'map' => [
+            'string_key' => 'Key "{key}" must be a string, got {type}.',
+        ]
+    ],
+]);
+```
+
 ## Extending Schemas
 
 You can extend existing schemas to reuse and build upon validation logic.
 
 ```php
 <?php
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\Type;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Type;
 
 $baseUserSchema = Schema::create([
     'name' => Type::string()->required(),

+ 5 - 6
composer.json

@@ -1,18 +1,18 @@
 {
-  "name": "phpdevcommunity/php-requestkit",
+  "name": "depo/php-requestkit",
   "description": "A lightweight and efficient PHP library for handling, validating, and transforming incoming HTTP requests, supporting both form submissions and JSON API payloads.",
   "type": "library",
   "license": "MIT",
   "authors": [
     {
       "name": "F. Michel",
-      "homepage": "https://www.phpdevcommunity.com"
+      "homepage": "https://www.depohub.org"
     }
   ],
   "autoload": {
     "psr-4": {
-      "PhpDevCommunity\\RequestKit\\": "src",
-      "Test\\PhpDevCommunity\\RequestKit\\": "tests"
+      "Depo\\RequestKit\\": "src",
+      "Test\\Depo\\RequestKit\\": "tests"
     },
     "files": [
       "functions/helpers.php"
@@ -21,10 +21,9 @@
   "require": {
     "php": ">=7.4",
     "ext-json": "*",
-    "phpdevcommunity/php-validator": "^1.0",
     "psr/http-message": "^1.0 || ^2.0"
   },
   "require-dev": {
-    "phpdevcommunity/unitester": "^0.1.0@alpha"
+    "depo/unitester": "^1.0.0"
   }
 }

+ 2 - 2
src/Builder/SchemaObjectFactory.php

@@ -1,8 +1,8 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Builder;
+namespace Depo\RequestKit\Builder;
 
-use PhpDevCommunity\RequestKit\Schema\Schema;
+use Depo\RequestKit\Schema\Schema;
 
 final class SchemaObjectFactory
 {

+ 3 - 3
src/Builder/TypeBuilder.php

@@ -1,9 +1,9 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Builder;
+namespace Depo\RequestKit\Builder;
 
-use PhpDevCommunity\RequestKit\Type;
-use PhpDevCommunity\RequestKit\Type\AbstractType;
+use Depo\RequestKit\Type;
+use Depo\RequestKit\Type\AbstractType;
 
 final class TypeBuilder
 {

+ 1 - 1
src/Exceptions/InvalidDataException.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Exceptions;
+namespace Depo\RequestKit\Exceptions;
 
 class InvalidDataException extends \Exception
 {

+ 3 - 3
src/Generator/DefinitionGenerator.php

@@ -1,9 +1,9 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Generator;
+namespace Depo\RequestKit\Generator;
 
-use PhpDevCommunity\RequestKit\Builder\SchemaObjectFactory;
-use PhpDevCommunity\RequestKit\Type;
+use Depo\RequestKit\Builder\SchemaObjectFactory;
+use Depo\RequestKit\Type;
 use ReflectionClass;
 use ReflectionException;
 use ReflectionProperty;

+ 5 - 5
src/Hydrator/ObjectHydrator.php

@@ -1,12 +1,12 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Hydrator;
+namespace Depo\RequestKit\Hydrator;
 
 use LogicException;
-use PhpDevCommunity\RequestKit\Type\ArrayOfType;
-use PhpDevCommunity\RequestKit\Type\ItemType;
-use PhpDevCommunity\RequestKit\Type\MapType;
-use PhpDevCommunity\RequestKit\Utils\KeyValueObject;
+use Depo\RequestKit\Type\ArrayOfType;
+use Depo\RequestKit\Type\ItemType;
+use Depo\RequestKit\Type\MapType;
+use Depo\RequestKit\Utils\KeyValueObject;
 use ReflectionClass;
 
 final class ObjectHydrator

+ 142 - 0
src/Locale.php

@@ -0,0 +1,142 @@
+<?php
+
+namespace Depo\RequestKit;
+
+final class Locale
+{
+    private static string $currentLocale = 'en';
+    private static array $messages = [];
+
+    /**
+     * Sets the active locale for error messages.
+     * @param string $locale e.g., 'en', 'fr'
+     */
+    public static function setLocale(string $locale): void
+    {
+        self::$currentLocale = $locale;
+    }
+
+    /**
+     * Adds a set of translation messages for a specific locale.
+     * @param string $locale
+     * @param array $messages
+     */
+    public static function addMessages(string $locale, array $messages): void
+    {
+        // array_replace_recursive is used to merge nested message arrays
+        self::$messages[$locale] = array_replace_recursive(self::$messages[$locale] ?? [], $messages);
+    }
+
+    /**
+     * Gets a translated message by its key.
+     *
+     * @param string $key The key of the message (e.g., 'error.required').
+     * @param array $params Placeholders to replace in the message.
+     * @return string
+     */
+    public static function get(string $key, array $params = []): string
+    {
+        if (empty(self::$messages['en'])) {
+            self::initializeDefaultMessages('en');
+        }
+        if (self::$currentLocale === 'fr' && empty(self::$messages['fr'])) {
+            self::initializeDefaultMessages('fr');
+        }
+
+        $messages = array_dot(self::$messages[self::$currentLocale]);
+        $message = $messages[$key] ?? null;
+        if ($message == null) {
+            $messages = array_dot( self::$messages['en']);
+            $message = $messages[$key] ?? $key;
+        }
+        unset($messages);
+        foreach ($params as $param => $value) {
+            $message = str_replace('{' . $param . '}', (string) $value, $message);
+        }
+
+        return $message;
+    }
+
+    /**
+     * Initializes the default English messages.
+     */
+    private static function initializeDefaultMessages(string $locale): void
+    {
+        if ($locale === 'en') {
+            self::addMessages('en', [
+                'error' => [
+                    'required' => 'Value is required, but got null or empty string.',
+                    'equals' => 'The value does not match the expected value.',
+                    'csrf' => 'Invalid CSRF token.',
+                    'json' => 'Invalid JSON input: {error}',
+                    'type' => [
+                        'string' => 'Value must be a string, got: {type}.',
+                        'int' => 'Value must be an integer, got: {type}.',
+                        'float' => 'Value must be a float, got: {type}.',
+                        'bool' => 'Value must be a boolean, got: {type}.',
+                        'numeric' => 'Value must be numeric, got: {type}.',
+                        'date' => 'Value must be a valid date.',
+                        'datetime' => 'Value must be a valid datetime.',
+                        'array' => 'Value must be an array.',
+                    ],
+                    'string' => [
+                        'min_length' => 'Value must be at least {min} characters long.',
+                        'max_length' => 'Value cannot be longer than {max} characters.',
+                        'email' => 'Value must be a valid email address.',
+                        'allowed' => 'Value is not allowed, allowed values are: {allowed}.',
+                    ],
+                    'int' => [
+                        'min' => 'Value must be at least {min}.',
+                        'max' => 'Value must be no more than {max}.',
+                    ],
+                    'array' => [
+                        'min_items' => 'Value must have at least {min} item(s).',
+                        'max_items' => 'Value must have at most {max} item(s).',
+                        'integer_keys' => 'All keys must be integers.',
+                    ],
+                    'map' => [
+                        'string_key' => 'Key "{key}" must be a string, got {type}.',
+                    ]
+                ],
+            ]);
+        }elseif ($locale === 'fr') {
+            self::addMessages('fr', [
+                'error' => [
+                    'required' => 'La valeur est requise.',
+                    'equals' => 'La valeur ne correspond pas à la valeur attendue.',
+                    'csrf' => 'Jeton CSRF invalide.',
+                    'json' => 'Entrée JSON invalide : {error}',
+                    'type' => [
+                        'string' => 'La valeur doit être une chaîne de caractères, reçu : {type}.',
+                        'int' => 'La valeur doit être un entier, reçu : {type}.',
+                        'float' => 'La valeur doit être un flottant, reçu : {type}.',
+                        'bool' => 'La valeur doit être un booléen, reçu : {type}.',
+                        'numeric' => 'La valeur doit être numérique, reçu : {type}.',
+                        'date' => 'La valeur doit être une date valide.',
+                        'datetime' => 'La valeur doit être une date et heure valide.',
+                        'array' => 'La valeur doit être un tableau.',
+                    ],
+                    'string' => [
+                        'min_length' => 'La valeur doit contenir au moins {min} caractères.',
+                        'max_length' => 'La valeur ne peut pas dépasser {max} caractères.',
+                        'email' => 'La valeur doit être une adresse e-mail valide.',
+                        'allowed' => 'La valeur n\'est pas autorisée. Les valeurs autorisées sont : {allowed}.',
+                    ],
+                    'int' => [
+                        'min' => 'La valeur doit être d\'au moins {min}.',
+                        'max' => 'La valeur ne doit pas dépasser {max}.',
+                    ],
+                    'array' => [
+                        'min_items' => 'La valeur doit contenir au moins {min} élément(s).',
+                        'max_items' => 'La valeur ne doit pas dépasser {max} élément(s).',
+                        'integer_keys' => 'Toutes les clés doivent être des entiers.',
+                    ],
+                    'map' => [
+                        'string_key' => 'La clé "{key}" doit être une chaîne de caractères, reçu {type}.',
+                    ]
+                ],
+            ]);
+        }
+
+    }
+}

+ 55 - 44
src/Schema/AbstractSchema.php

@@ -1,19 +1,20 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Schema;
+namespace Depo\RequestKit\Schema;
 
-use Exception;
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
-use PhpDevCommunity\RequestKit\Type\AbstractType;
-use PhpDevCommunity\RequestKit\Type\ArrayOfType;
-use PhpDevCommunity\RequestKit\Type\ItemType;
+use Depo\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\Type\AbstractType;
+use Depo\RequestKit\Type\ArrayOfType;
+use Depo\RequestKit\Type\ItemType;
 use Psr\Http\Message\ServerRequestInterface;
 
 abstract class AbstractSchema
 {
     protected bool $patchMode = false;
     protected string $title = '';
-    protected string $version = '1.0';
+    protected string $version = '2.0';
+    protected array $headerDefinitions = [];
 
     final public function patch(): self
     {
@@ -38,52 +39,53 @@ abstract class AbstractSchema
         return $this;
     }
 
-    /**
-     * @param string $json
-     * @param int $depth
-     * @param int $flags
-     * @return SchemaAccessor
-     * @throws InvalidDataException
-     */
+    final public function withHeaders(array $definitions): self
+    {
+        $this->headerDefinitions = $definitions;
+        return $this;
+    }
+
     final public function processJsonInput(string $json, int $depth = 512, int $flags = 0): SchemaAccessor
     {
-        $data = json_decode($json, true, $depth , $flags);
+        $data = json_decode($json, true, $depth, $flags);
         if (json_last_error() !== JSON_ERROR_NONE) {
             $errorMessage = json_last_error_msg();
-            throw new InvalidDataException($errorMessage);
+            throw new InvalidDataException(Locale::get('error.json', ['error' => $errorMessage]));
         }
         return $this->process($data);
     }
 
-    /**
-     * @param ServerRequestInterface $request
-     * @return SchemaAccessor
-     * @throws InvalidDataException
-     */
     final public function processHttpRequest(ServerRequestInterface $request): SchemaAccessor
     {
+        $this->validateHeaders($request);
+
         if (in_array('application/json', $request->getHeader('Content-Type'))) {
             return $this->processJsonInput($request->getBody()->getContents());
         }
-        return $this->process($request->getParsedBody());
+
+        return $this->processFormHttpRequest($request);
+    }
+
+    final public function processFormHttpRequest(ServerRequestInterface $request, ?string $expectedToken = null, string $csrfKey = '_csrf'): SchemaAccessor
+    {
+        $this->validateHeaders($request);
+        $data = $request->getParsedBody();
+
+        if ($expectedToken !== null) {
+            if (!isset($data[$csrfKey]) || !hash_equals($expectedToken, $data[$csrfKey])) {
+                throw new InvalidDataException(Locale::get('error.csrf'));
+            }
+            unset($data[$csrfKey]);
+        }
+
+        return $this->process($data);
     }
 
-    /**
-     * @param ServerRequestInterface $request
-     * @return SchemaAccessor
-     * @throws InvalidDataException
-     */
     final public function processHttpQuery(ServerRequestInterface $request): SchemaAccessor
     {
         return $this->process($request->getQueryParams(), true);
     }
 
-    /**
-     * @param array $data
-     * @param bool $allowEmptyData
-     * @return SchemaAccessor
-     * @throws InvalidDataException
-     */
     final public function process(array $data, bool $allowEmptyData = false): SchemaAccessor
     {
         $accessor = new SchemaAccessor($data, $this, $allowEmptyData);
@@ -91,11 +93,27 @@ abstract class AbstractSchema
         return $accessor;
     }
 
-    /**
-     * @return array<string, AbstractType>
-     */
     abstract protected function definitions(): array;
 
+    private function validateHeaders(ServerRequestInterface $request): void
+    {
+        if (empty($this->headerDefinitions)) {
+            return;
+        }
+
+        $headerData = [];
+        foreach ($request->getHeaders() as $name => $values) {
+            $headerData[strtolower($name)] = $values[0] ?? null;
+        }
+        $headerDefinitions = [];
+        foreach ($this->headerDefinitions as $name => $definition) {
+            $headerDefinitions[strtolower($name)] = clone $definition;
+        }
+
+        $headerSchema = Schema::create($headerDefinitions);
+        $headerSchema->process($headerData);
+    }
+
     private function getDefinitions(): array
     {
         $definitions = $this->definitions();
@@ -107,10 +125,7 @@ abstract class AbstractSchema
         return $definitions;
     }
 
-    /**
-     * @return array<string, AbstractType>
-     */
-    final public function copyDefinitions() : array
+    final public function copyDefinitions(): array
     {
         $definitions = [];
         foreach ($this->getDefinitions() as $key => $definition) {
@@ -127,10 +142,6 @@ abstract class AbstractSchema
                 $data[$key] = $definition->getExample() ?: $definition->copySchema()->generateExampleData();
                 continue;
             }
-            if ($definition instanceof ArrayOfType) {
-                $data[$key][] = $definition->getExample();
-                continue;
-            }
             $data[$key] = $definition->getExample();
         }
         return $data;

+ 4 - 4
src/Schema/Schema.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Schema;
+namespace Depo\RequestKit\Schema;
 
-use PhpDevCommunity\RequestKit\Builder\SchemaObjectFactory;
-use PhpDevCommunity\RequestKit\Generator\DefinitionGenerator;
-use PhpDevCommunity\RequestKit\Type\AbstractType;
+use Depo\RequestKit\Builder\SchemaObjectFactory;
+use Depo\RequestKit\Generator\DefinitionGenerator;
+use Depo\RequestKit\Type\AbstractType;
 use ReflectionException;
 
 final class Schema extends AbstractSchema

+ 4 - 4
src/Schema/SchemaAccessor.php

@@ -1,11 +1,11 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Schema;
+namespace Depo\RequestKit\Schema;
 
 use InvalidArgumentException;
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
-use PhpDevCommunity\RequestKit\Hydrator\ObjectHydrator;
-use PhpDevCommunity\RequestKit\Type\ItemType;
+use Depo\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Hydrator\ObjectHydrator;
+use Depo\RequestKit\Type\ItemType;
 
 final class SchemaAccessor
 {

+ 16 - 16
src/Type.php

@@ -1,20 +1,20 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit;
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\Type\AbstractType;
-use PhpDevCommunity\RequestKit\Type\ArrayOfType;
-use PhpDevCommunity\RequestKit\Type\BoolType;
-use PhpDevCommunity\RequestKit\Type\DateTimeType;
-use PhpDevCommunity\RequestKit\Type\DateType;
-use PhpDevCommunity\RequestKit\Type\EmailType;
-use PhpDevCommunity\RequestKit\Type\FloatType;
-use PhpDevCommunity\RequestKit\Type\IntType;
-use PhpDevCommunity\RequestKit\Type\ItemType;
-use PhpDevCommunity\RequestKit\Type\MapType;
-use PhpDevCommunity\RequestKit\Type\NumericType;
-use PhpDevCommunity\RequestKit\Type\StringType;
-use PhpDevCommunity\RequestKit\Utils\DateOnly;
+namespace Depo\RequestKit;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Type\AbstractType;
+use Depo\RequestKit\Type\ArrayOfType;
+use Depo\RequestKit\Type\BoolType;
+use Depo\RequestKit\Type\DateTimeType;
+use Depo\RequestKit\Type\DateType;
+use Depo\RequestKit\Type\EmailType;
+use Depo\RequestKit\Type\FloatType;
+use Depo\RequestKit\Type\IntType;
+use Depo\RequestKit\Type\ItemType;
+use Depo\RequestKit\Type\MapType;
+use Depo\RequestKit\Type\NumericType;
+use Depo\RequestKit\Type\StringType;
+use Depo\RequestKit\Utils\DateOnly;
 
 final class Type
 {
@@ -75,7 +75,7 @@ final class Type
 
     public static function typeObject(string $type): ?AbstractType
     {
-        if ($type=== DateOnly::class) {
+        if ($type === DateOnly::class) {
             return self::date();
         }
 

+ 4 - 4
src/Type/AbstractStringType.php

@@ -1,8 +1,8 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\ValidationResult;
+use Depo\RequestKit\ValidationResult;
 use PhpDevCommunity\Validator\Assert\StringLength;
 
 abstract class AbstractStringType extends AbstractType
@@ -71,7 +71,7 @@ abstract class AbstractStringType extends AbstractType
             if (empty($value) || !is_string($value)) {
                 return $value;
             }
-            return mb_strtoupper($value);
+            return function_exists('mb_strtoupper') ? mb_strtoupper($value) : strtoupper($value);
         });
         return $this;
     }
@@ -82,7 +82,7 @@ abstract class AbstractStringType extends AbstractType
             if (empty($value) || !is_string($value)) {
                 return $value;
             }
-            return mb_strtolower($value);
+            return function_exists('mb_strtoupper') ? mb_strtolower($value) : strtolower($value);
         });
         return $this;
     }

+ 4 - 5
src/Type/AbstractType.php

@@ -1,9 +1,10 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
 use Closure;
-use PhpDevCommunity\RequestKit\ValidationResult;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\ValidationResult;
 
 abstract class AbstractType
 {
@@ -17,7 +18,6 @@ abstract class AbstractType
     protected $default = null;
     protected ?array $transformers = null;
 
-
     final public function required(): self
     {
         $this->required = true;
@@ -114,14 +114,13 @@ abstract class AbstractType
 
         if ($result->getValue() === null || (is_string($result->getValue())) && trim($result->getValue()) === '') {
             if ($this->isRequired()) {
-                $result->setError("Value is required, but got null or empty string");
+                $result->setError(Locale::get('error.required'));
             }
             return $result;
         }
 
         $this->transformValue($result);
         $this->validateValue($result);
-
         return $result;
     }
 

+ 9 - 8
src/Type/ArrayOfType.php

@@ -1,10 +1,11 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\ValidationResult;
+use Depo\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\ValidationResult;
 
 final class ArrayOfType extends AbstractType
 {
@@ -69,24 +70,24 @@ final class ArrayOfType extends AbstractType
             $values = array_filter($values, fn($v) => $v !== '');
         }
         if (!is_array($values)) {
-            $result->setError('Value must be an array');
+            $result->setError(Locale::get('error.type.array'));
             return;
         }
 
         $definitions = [];
         $count = count($values);
         if ($this->min && $count < $this->min) {
-            $result->setError("Value must have at least $this->min item(s)");
+            $result->setError(Locale::get('error.array.min_items', ['min' => $this->min]));
             return;
         }
         if ($this->max && $count > $this->max) {
-            $result->setError("Value must have at most $this->max item(s)");
+            $result->setError(Locale::get('error.array.max_items', ['max' => $this->max]));
             return;
         }
 
         foreach ($values as $key => $value) {
             if ($this->acceptStringKeys === false && !is_int($key)) {
-                $result->setError('All keys must be integers');
+                $result->setError(Locale::get('error.array.integer_keys'));
                 return;
             }
             if (is_string($key)) {

+ 17 - 15
src/Type/BoolType.php

@@ -1,37 +1,39 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\Type\Traits\StrictTrait;
-use PhpDevCommunity\RequestKit\ValidationResult;
-use PhpDevCommunity\Validator\Assert\Boolean;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\Type\Traits\EqualTrait;
+use Depo\RequestKit\Type\Traits\StrictTrait;
+use Depo\RequestKit\ValidationResult;
 
 final class BoolType extends AbstractType
 {
-
     use StrictTrait;
+    use EqualTrait;
 
     protected function validateValue(ValidationResult $result): void
     {
-        if ($this->isStrict() && !is_bool($result->getValue())) {
-            $result->setError("Value must be a boolean, got: " . gettype($result->getValue()));
+        $value = $result->getValue();
+
+        if ($this->isStrict() && !is_bool($value)) {
+            $result->setError(Locale::get('error.type.bool', ['type' => gettype($value)]));
             return;
         }
 
-        if ($this->isStrict() === false && !is_bool($result->getValue())) {
-            if (in_array($result->getValue(), [1, '1', 'true', 'on', 'TRUE', 'ON'], true)) {
+        if ($this->isStrict() === false && !is_bool($value)) {
+            if (in_array($value, [1, '1', 'true', 'on', 'TRUE', 'ON'], true)) {
                 $result->setValue(true);
-            }elseif (in_array($result->getValue(), [0, '0', 'false', 'off', 'FALSE', 'OFF'], true)) {
+            } elseif (in_array($value, [0, '0', 'false', 'off', 'FALSE', 'OFF'], true)) {
                 $result->setValue(false);
-            }else {
-                $result->setError("Value must be a boolean, got: " . gettype($result->getValue()));
+            } else {
+                $result->setError(Locale::get('error.type.bool', ['type' => gettype($value)]));
                 return;
             }
         }
 
-        $validator = new Boolean();
-        if ($validator->validate($result->getValue()) === false) {
-            $result->setError($validator->getError());
+        if ($this->checkEquals && $result->getValue() !== $this->equalTo) {
+            $result->setError(Locale::get('error.equals'));
         }
     }
 }

+ 17 - 10
src/Type/DateTimeType.php

@@ -1,14 +1,14 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\ValidationResult;
-use PhpDevCommunity\Validator\Assert\Email;
-use PhpDevCommunity\Validator\Assert\StringLength;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\ValidationResult;
 
 final class DateTimeType extends AbstractType
 {
     private string $format = 'Y-m-d H:i:s';
+
     public function format(string $format): self
     {
         $this->format = $format;
@@ -17,17 +17,24 @@ final class DateTimeType extends AbstractType
 
     protected function validateValue(ValidationResult $result): void
     {
-        if (is_string($result->getValue())) {
-            $datetime = \DateTime::createFromFormat($this->format, $result->getValue());
-            if ($datetime === false) {
-                $result->setError("Value must be a valid datetime for format: " . $this->format);
+        $value = $result->getValue();
+        if ($value instanceof \DateTimeInterface) {
+            return;
+        }
+
+        if (is_string($value)) {
+            $datetime = \DateTime::createFromFormat($this->format, $value);
+            if ($datetime === false || $datetime->format($this->format) !== $value) {
+                $result->setError(Locale::get('error.type.datetime'));
                 return;
             }
             $result->setValue($datetime);
-        }elseif (is_int($result->getValue())) {
+        } elseif (is_int($value)) {
             $datetime = new \DateTime();
-            $datetime->setTimestamp($result->getValue());
+            $datetime->setTimestamp($value);
             $result->setValue($datetime);
+        } else {
+            $result->setError(Locale::get('error.type.datetime'));
         }
     }
 }

+ 20 - 11
src/Type/DateType.php

@@ -1,33 +1,42 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\Utils\DateOnly;
-use PhpDevCommunity\RequestKit\ValidationResult;
-use PhpDevCommunity\Validator\Assert\Email;
-use PhpDevCommunity\Validator\Assert\StringLength;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\Utils\DateOnly;
+use Depo\RequestKit\ValidationResult;
 
 final class DateType extends AbstractType
 {
     private string $format = 'Y-m-d';
+
     public function format(string $format): self
     {
         $this->format = $format;
         return $this;
     }
+
     protected function validateValue(ValidationResult $result): void
     {
-        if (is_string($result->getValue())) {
-            $datetime = DateOnly::createFromFormat($this->format, $result->getValue());
-            if ($datetime === false) {
-                $result->setError("Value must be a valid date for format: " . $this->format);
+        $value = $result->getValue();
+
+        if ($value instanceof \DateTimeInterface) {
+            return;
+        }
+
+        if (is_string($value)) {
+            $datetime = DateOnly::createFromFormat($this->format, $value);
+            if ($datetime === false || $datetime->format($this->format) !== $value) {
+                $result->setError(Locale::get('error.type.date'));
                 return;
             }
             $result->setValue($datetime);
-        }elseif (is_int($result->getValue())) {
+        } elseif (is_int($value)) {
             $datetime = new DateOnly();
-            $datetime->setTimestamp($result->getValue());
+            $datetime->setTimestamp($value);
             $result->setValue($datetime);
+        } else {
+            $result->setError(Locale::get('error.type.date'));
         }
     }
 }

+ 5 - 7
src/Type/EmailType.php

@@ -1,18 +1,16 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\ValidationResult;
-use PhpDevCommunity\Validator\Assert\Email;
-use PhpDevCommunity\Validator\Assert\StringLength;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\ValidationResult;
 
 final class EmailType extends AbstractStringType
 {
     protected function validateValue(ValidationResult $result): void
     {
-        $validator = new Email();
-        if ($validator->validate($result->getValue()) === false) {
-            $result->setError($validator->getError());
+        if (filter_var($result->getValue(), FILTER_VALIDATE_EMAIL) === false) {
+            $result->setError(Locale::get('error.string.email'));
         }
     }
 }

+ 20 - 11
src/Type/FloatType.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\Type\Traits\StrictTrait;
-use PhpDevCommunity\RequestKit\ValidationResult;
-use PhpDevCommunity\Validator\Assert\Numeric;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\Type\Traits\StrictTrait;
+use Depo\RequestKit\ValidationResult;
 
 final class FloatType extends AbstractType
 {
@@ -28,18 +28,27 @@ final class FloatType extends AbstractType
     protected function validateValue(ValidationResult $result): void
     {
         if ($this->isStrict() && !is_float($result->getValue())) {
-            $result->setError("Value must be a float, got: " . gettype($result->getValue()));
+            $result->setError(Locale::get('error.type.float', ['type' => gettype($result->getValue())]));
             return;
         }
 
-        if ($this->isStrict() === false && is_numeric($result->getValue())) {
-            $value = floatval($result->getValue());
-            $result->setValue($value);
+        if (!$this->isStrict() && !is_numeric($result->getValue())) {
+            $result->setError(Locale::get('error.type.float', ['type' => gettype($result->getValue())]));
+            return;
+        }
+
+        if (!$this->isStrict()) {
+            $result->setValue(floatval($result->getValue()));
         }
 
-        $validator = new Numeric();
-        if ($validator->validate($result->getValue()) === false) {
-            $result->setError($validator->getError());
+        if ($this->min && $result->getValue() < $this->min) {
+            $result->setError(Locale::get('error.int.min', ['min' => $this->min]));
+            return;
+        }
+
+        if ($this->max && $result->getValue() > $this->max) {
+            $result->setError(Locale::get('error.int.max', ['max' => $this->max]));
+            return;
         }
     }
 }

+ 25 - 15
src/Type/IntType.php

@@ -1,14 +1,16 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\Type\Traits\StrictTrait;
-use PhpDevCommunity\RequestKit\ValidationResult;
-use PhpDevCommunity\Validator\Assert\Integer;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\Type\Traits\EqualTrait;
+use Depo\RequestKit\Type\Traits\StrictTrait;
+use Depo\RequestKit\ValidationResult;
 
 final class IntType extends AbstractType
 {
     use StrictTrait;
+    use EqualTrait;
 
     private ?int $min = null;
     private ?int $max = null;
@@ -28,24 +30,32 @@ final class IntType extends AbstractType
     protected function validateValue(ValidationResult $result): void
     {
         if ($this->isStrict() && !is_int($result->getValue())) {
-            $result->setError("Value must be a int, got: " . gettype($result->getValue()));
+            $result->setError(Locale::get('error.type.int', ['type' => gettype($result->getValue())]));
             return;
         }
 
-        if ($this->isStrict() === false && is_numeric($result->getValue())) {
-            $value = intval($result->getValue());
-            $result->setValue($value);
+        if (!$this->isStrict() && !is_numeric($result->getValue())) {
+            $result->setError(Locale::get('error.type.int', ['type' => gettype($result->getValue())]));
+            return;
+        }
+        
+        if (!$this->isStrict()) {
+            $result->setValue(intval($result->getValue()));
         }
 
-        $validator = new Integer();
-        if ($this->min) {
-            $validator->min($this->min);
+        if ($this->checkEquals && $result->getValue() !== $this->equalTo) {
+            $result->setError(Locale::get('error.equals'));
+            return;
         }
-        if ($this->max) {
-            $validator->max($this->max);
+
+        if ($this->min !== null && $result->getValue() < $this->min) {
+            $result->setError(Locale::get('error.int.min', ['min' => $this->min]));
+            return;
         }
-        if ($validator->validate($result->getValue()) === false) {
-            $result->setError($validator->getError());
+
+        if ($this->max !== null && $result->getValue() > $this->max) {
+            $result->setError(Locale::get('error.int.max', ['max' => $this->max]));
+            return;
         }
     }
 }

+ 6 - 5
src/Type/ItemType.php

@@ -1,10 +1,11 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\ValidationResult;
+use Depo\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\ValidationResult;
 
 final class ItemType extends AbstractType
 {
@@ -40,7 +41,7 @@ final class ItemType extends AbstractType
     {
         $value = $result->getValue();
         if (!is_array($value)) {
-            $result->setError("Value must be an array, got: " . gettype($result->getValue()));
+            $result->setError(Locale::get('error.type.array'));
             return;
         }
         try {

+ 12 - 11
src/Type/MapType.php

@@ -1,11 +1,12 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\Utils\KeyValueObject;
-use PhpDevCommunity\RequestKit\ValidationResult;
+use Depo\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Utils\KeyValueObject;
+use Depo\RequestKit\ValidationResult;
 
 final class MapType extends AbstractType
 {
@@ -51,24 +52,24 @@ final class MapType extends AbstractType
         }
         $values = $result->getValue();
         if (!is_array($values) && !$values instanceof KeyValueObject) {
-            $result->setError('Value must be an array or KeyValueObject');
+            $result->setError(Locale::get('error.type.array'));
             return;
         }
 
         $count = count($values);
-        if ($this->min && $count < $this->min) {
-            $result->setError("Value must have at least $this->min item(s)");
+        if ($this->min !== null && $count < $this->min) {
+            $result->setError(Locale::get('error.array.min_items', ['min' => $this->min]));
             return;
         }
-        if ($this->max && $count > $this->max) {
-            $result->setError("Value must have at most $this->max item(s)");
+        if ($this->max !== null && $count > $this->max) {
+            $result->setError(Locale::get('error.array.max_items', ['max' => $this->max]));
             return;
         }
 
         $definitions = [];
         foreach ($values as $key => $value) {
             if (!is_string($key)) {
-                $result->setError(sprintf( 'Key "%s" must be a string, got %s', $key, gettype($key)));
+                $result->setError(Locale::get('error.map.string_key', ['key' => $key, 'type' => gettype($key)]));
                 return;
             }
             $key = trim($key);

+ 13 - 10
src/Type/NumericType.php

@@ -1,23 +1,26 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\Type\Traits\StrictTrait;
-use PhpDevCommunity\RequestKit\ValidationResult;
-use PhpDevCommunity\Validator\Assert\Numeric;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\Type\Traits\EqualTrait;
+use Depo\RequestKit\ValidationResult;
 
 final class NumericType extends AbstractType
 {
+    use EqualTrait;
+
     protected function validateValue(ValidationResult $result): void
     {
-        if (is_numeric($result->getValue())) {
-            $value = strval($result->getValue());
-            $result->setValue($value);
+        if (!is_numeric($result->getValue())) {
+            $result->setError(Locale::get('error.type.numeric', ['type' => gettype($result->getValue())]));
+            return;
         }
 
-        $validator = new Numeric();
-        if ($validator->validate($result->getValue()) === false) {
-            $result->setError($validator->getError());
+        $result->setValue(strval($result->getValue()));
+
+        if ($this->checkEquals && $result->getValue() !== $this->equalTo) {
+            $result->setError(Locale::get('error.equals'));
         }
     }
 }

+ 26 - 21
src/Type/StringType.php

@@ -1,14 +1,16 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type;
+namespace Depo\RequestKit\Type;
 
-use PhpDevCommunity\RequestKit\Type\Traits\StrictTrait;
-use PhpDevCommunity\RequestKit\ValidationResult;
-use PhpDevCommunity\Validator\Assert\StringLength;
+use Depo\RequestKit\Locale;
+use Depo\RequestKit\Type\Traits\EqualTrait;
+use Depo\RequestKit\Type\Traits\StrictTrait;
+use Depo\RequestKit\ValidationResult;
 
 final class StringType extends AbstractStringType
 {
     use StrictTrait;
+    use EqualTrait;
     private array $allowed = [];
     private ?int $min = null;
     private ?int $max = null;
@@ -29,35 +31,38 @@ final class StringType extends AbstractStringType
     protected function validateValue(ValidationResult $result): void
     {
         if ($this->isStrict() && !is_string($result->getValue())) {
-            $result->setError("Value must be a string, got: " . gettype($result->getValue()));
+            $result->setError(Locale::get('error.type.string', ['type' => gettype($result->getValue())]));
             return;
         }
 
-        if ($this->isStrict() === false && !is_string($result->getValue())) {
-
-            if (is_array($result->getValue())) {
-                $result->setError("Value must be a string, got: array");
+        if (!$this->isStrict() && !is_string($result->getValue())) {
+            if (!is_scalar($result->getValue())) {
+                $result->setError(Locale::get('error.type.string', ['type' => gettype($result->getValue())]));
                 return;
             }
-
-            $value = strval($result->getValue());
-            $result->setValue($value);
+            $result->setValue(strval($result->getValue()));
         }
 
-        if (!empty($this->allowed) && !in_array($result->getValue(), $this->allowed, $this->isStrict())) {
-            $result->setError("Value is not allowed, allowed values are: " . implode(", ", $this->allowed));
+        if ($this->checkEquals && $result->getValue() !== $this->equalTo) {
+            $result->setError(Locale::get('error.equals'));
             return;
         }
 
-        $validator = new StringLength();
-        if ($this->min) {
-            $validator->min($this->min);
+
+        if (!empty($this->allowed) && !in_array($result->getValue(), $this->allowed, true)) {
+            $result->setError(Locale::get('error.string.allowed', ['allowed' => implode(", ", $this->allowed)]));
+            return;
         }
-        if ($this->max) {
-            $validator->max($this->max);
+
+        $valueLength = function_exists('mb_strlen') ? mb_strlen($result->getValue()) : strlen($result->getValue());
+        if ($this->min !== null && $valueLength < $this->min) {
+            $result->setError(Locale::get('error.string.min_length', ['min' => $this->min]));
+            return;
         }
-        if ($validator->validate($result->getValue()) === false) {
-            $result->setError($validator->getError());
+
+        if ($this->max !== null && $valueLength > $this->max) {
+            $result->setError(Locale::get('error.string.max_length', ['max' => $this->max]));
+            return;
         }
     }
 }

+ 22 - 0
src/Type/Traits/EqualTrait.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace Depo\RequestKit\Type\Traits;
+
+trait EqualTrait
+{
+
+    /**
+     * @var mixed
+     */
+    protected $equalTo = null;
+    protected bool $checkEquals = false;
+
+    final public function equals($expectedValue)
+    {
+        $this->equalTo = $expectedValue;
+        $this->checkEquals = true;
+        return $this;
+    }
+
+
+}

+ 1 - 1
src/Type/Traits/StrictTrait.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Type\Traits;
+namespace Depo\RequestKit\Type\Traits;
 
 trait StrictTrait
 {

+ 1 - 1
src/Utils/DateOnly.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Utils;
+namespace Depo\RequestKit\Utils;
 
 final class DateOnly extends \DateTime
 {

+ 1 - 1
src/Utils/KeyValueObject.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit\Utils;
+namespace Depo\RequestKit\Utils;
 
 final class KeyValueObject extends \ArrayObject implements \JsonSerializable
 {

+ 2 - 2
src/ValidationResult.php

@@ -1,8 +1,8 @@
 <?php
 
-namespace PhpDevCommunity\RequestKit;
+namespace Depo\RequestKit;
 
-use PhpDevCommunity\RequestKit\Schema\SchemaAccessor;
+use Depo\RequestKit\Schema\SchemaAccessor;
 
 final class ValidationResult
 {

+ 9 - 9
tests/HydratorTest.php

@@ -1,16 +1,16 @@
 <?php
 
-namespace Test\PhpDevCommunity\RequestKit;
+namespace Test\Depo\RequestKit;
 
 use DateTime;
-use PhpDevCommunity\RequestKit\Builder\RequestKitBuilderFactory;
-use PhpDevCommunity\RequestKit\Builder\SchemaObjectFactory;
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\Type;
-use PhpDevCommunity\UniTester\TestCase;
-use Test\PhpDevCommunity\RequestKit\Model\AddressTest;
-use Test\PhpDevCommunity\RequestKit\Model\UserModelTest;
+use Depo\RequestKit\Builder\RequestKitBuilderFactory;
+use Depo\RequestKit\Builder\SchemaObjectFactory;
+use Depo\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Type;
+use Depo\UniTester\TestCase;
+use Test\Depo\RequestKit\Model\AddressTest;
+use Test\Depo\RequestKit\Model\UserModelTest;
 
 class HydratorTest extends TestCase
 {

+ 87 - 0
tests/LocaleTest.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace Test\Depo\RequestKit;
+
+use Depo\RequestKit\Locale;
+use Depo\UniTester\TestCase;
+
+class LocaleTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        // Reset locale to default before each test group
+        Locale::setLocale('en');
+    }
+
+    protected function tearDown(): void
+    {
+        // Clean up after tests
+        Locale::setLocale('en');
+    }
+
+    protected function execute(): void
+    {
+        $this->testGetDefaultEnglishMessage();
+        $this->testSetLocaleToFrench();
+        $this->testMessageWithParameters();
+        $this->testFallbackToEnglish();
+        $this->testAddNewLanguage();
+    }
+
+    private function testGetDefaultEnglishMessage()
+    {
+        $message = Locale::get('error.required');
+        $this->assertStrictEquals('Value is required, but got null or empty string.', $message);
+    }
+
+    private function testSetLocaleToFrench()
+    {
+        Locale::setLocale('fr');
+        $message = Locale::get('error.required');
+        $this->assertStrictEquals('La valeur est requise.', $message);
+    }
+
+    private function testMessageWithParameters()
+    {
+        // English
+        Locale::setLocale('en');
+        $messageEn = Locale::get('error.string.min_length', ['min' => 5]);
+        $this->assertStrictEquals('Value must be at least 5 characters long.', $messageEn);
+
+        // French
+        Locale::setLocale('fr');
+        $messageFr = Locale::get('error.string.min_length', ['min' => 10]);
+        $this->assertStrictEquals('La valeur doit contenir au moins 10 caractères.', $messageFr);
+    }
+
+    private function testFallbackToEnglish()
+    {
+        // Add a message only in English
+        Locale::addMessages('en', ['error.test' => 'This is a test.']);
+        
+        // Set locale to French
+        Locale::setLocale('fr');
+
+        // The key 'error.test' does not exist in French, so it should fall back to English.
+        $message = Locale::get('error.test');
+        $this->assertStrictEquals('This is a test.', $message);
+    }
+
+    private function testAddNewLanguage()
+    {
+        $spanishMessages = [
+            'error' => [
+                'required' => 'El valor es requerido.'
+            ]
+        ];
+        Locale::addMessages('es', $spanishMessages);
+        Locale::setLocale('es');
+
+        $message = Locale::get('error.required');
+        $this->assertStrictEquals('El valor es requerido.', $message);
+
+        // Check that it falls back to English for non-translated messages
+        $messageEquals = Locale::get('error.equals');
+        $this->assertStrictEquals('The value does not match the expected value.', $messageEquals);
+    }
+}

+ 1 - 1
tests/Model/AddressTest.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace Test\PhpDevCommunity\RequestKit\Model;
+namespace Test\Depo\RequestKit\Model;
 
 class AddressTest
 {

+ 2 - 2
tests/Model/UserModelTest.php

@@ -1,8 +1,8 @@
 <?php
 
-namespace Test\PhpDevCommunity\RequestKit\Model;
+namespace Test\Depo\RequestKit\Model;
 
-use PhpDevCommunity\RequestKit\Utils\DateOnly;
+use Depo\RequestKit\Utils\DateOnly;
 
 class UserModelTest
 {

+ 209 - 9
tests/SchemaTest.php

@@ -1,13 +1,16 @@
 <?php
 
-namespace Test\PhpDevCommunity\RequestKit;
+namespace Test\Depo\RequestKit;
 
 use DateTime;
-use PhpDevCommunity\RequestKit\Exceptions\InvalidDataException;
-use PhpDevCommunity\RequestKit\Schema\Schema;
-use PhpDevCommunity\RequestKit\Type;
-use PhpDevCommunity\RequestKit\Utils\KeyValueObject;
-use PhpDevCommunity\UniTester\TestCase;
+use Depo\RequestKit\Exceptions\InvalidDataException;
+use Depo\RequestKit\Schema\Schema;
+use Depo\RequestKit\Type;
+use Depo\RequestKit\Utils\KeyValueObject;
+use Depo\UniTester\TestCase;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UriInterface;
 
 class SchemaTest extends TestCase
 {
@@ -45,9 +48,117 @@ class SchemaTest extends TestCase
         $this->testArray();
         $this->testExtend();
         $this->testExampleData();
+        $this->testWithHeaders();
+        $this->testProcessForm();
+    }
+
+    public function testWithHeaders()
+    {
+        $schema = Schema::create([
+            'name' => Type::string()->required(),
+        ])->withHeaders([
+            'Content-Type' => Type::string()->equals('application/json'),
+            'X-Api-Key' => Type::string()->required()->length(10),
+        ]);
+
+        // Test case 1: Valid headers - should not throw an exception
+        $request = $this->createServerRequest(
+            ['Content-Type' => 'application/json', 'X-Api-Key' => '1234567890'],
+            ['name' => 'test']
+        );
+        $result = $schema->processHttpRequest($request);
+        $this->assertEquals('test', $result->get('name')); // Assert data is processed
+
+        // Test case 2: Missing required header
+        $request = $this->createServerRequest(
+            ['Content-Type' => 'application/json'],
+            ['name' => 'test']
+        );
+        $this->expectException(InvalidDataException::class, function () use ($schema, $request) {
+            try {
+                $schema->processHttpRequest($request);
+            } catch (InvalidDataException $e) {
+                $this->assertNotEmpty($e->getError('x-api-key'));
+                throw $e;
+            }
+        });
 
+        // Test case 3: Header constraint violation
+        $request = $this->createServerRequest(
+            ['Content-Type' => 'application/xml', 'X-Api-Key' => '1234567890'],
+            ['name' => 'test']
+        );
+        $this->expectException(InvalidDataException::class, function () use ($schema, $request) {
+            try {
+                $schema->processHttpRequest($request);
+            } catch (InvalidDataException $e) {
+                $this->assertNotEmpty($e->getError('content-type'));
+                throw $e;
+            }
+        });
     }
 
+    public function testProcessForm()
+    {
+        $schema = Schema::create([
+            'username' => Type::string()->required(),
+        ]);
+        $csrfToken = 'a_very_secret_token_123';
+
+        // Test case 1: Valid form with correct CSRF token - should not throw an exception
+        $request = $this->createServerRequest(
+            [
+                'Content-Type' => 'application/x-www-form-urlencoded',
+            ],
+            ['username' => 'john.doe', '_csrf' => $csrfToken]
+        );
+        $result = $schema->processFormHttpRequest($request, $csrfToken);
+        $this->assertEquals('john.doe', $result->get('username'));
+
+        // Test case 2: Valid form without CSRF check (optional) - should not throw an exception
+        $request = $this->createServerRequest(
+            [
+                'Content-Type' => 'application/x-www-form-urlencoded',
+            ],
+            ['username' => 'jane.doe']
+        );
+        $result = $schema->processFormHttpRequest($request, null); // Pass null to skip CSRF
+        $this->assertEquals('jane.doe', $result->get('username'));
+
+        // Test case 3: Form with incorrect CSRF token
+        $request = $this->createServerRequest(
+            [
+                'Content-Type' => 'application/x-www-form-urlencoded',
+            ],
+            ['username' => 'hacker', '_csrf' => 'wrong_token'],
+        );
+        $this->expectException(InvalidDataException::class, function () use ($schema, $request, $csrfToken) {
+            try {
+                $schema->processFormHttpRequest($request, $csrfToken);
+            } catch (InvalidDataException $e) {
+                $this->assertEquals('Invalid CSRF token.', $e->getMessage());
+                throw $e;
+            }
+        });
+
+        // Test case 4: Form with missing CSRF token
+        $request = $this->createServerRequest(
+            [
+                'Content-Type' => 'application/x-www-form-urlencoded',
+            ],
+            ['username' => 'hacker'],
+        );
+        $this->expectException(InvalidDataException::class, function () use ($schema, $request, $csrfToken) {
+            try {
+                $schema->processFormHttpRequest($request, $csrfToken);
+            } catch (InvalidDataException $e) {
+                $this->assertEquals('Invalid CSRF token.', $e->getMessage());
+                throw $e;
+            }
+        });
+    }
+
+
     public function testValidData(): void
     {
         $data = [
@@ -337,7 +448,7 @@ class SchemaTest extends TestCase
         $schema1 = Schema::create([
             'name' => Type::string()->length(20, 50)->required()->example('John Doe'),
             'age' => Type::int()->strict()->alias('my_age')->example(20),
-            'roles' => Type::arrayOf(Type::string()->strict())->required()->example('admin'),
+            'roles' => Type::arrayOf(Type::string()->strict())->required()->example(['admin']),
             'address' => Type::item([
                 'street' => Type::string()->length(15, 100),
                 'city' => Type::string()->allowed('Paris', 'London'),
@@ -361,7 +472,11 @@ class SchemaTest extends TestCase
         $schema2 = Schema::create([
             'name' => Type::string()->length(20, 50)->required()->example('John Doe'),
             'age' => Type::int()->strict()->alias('my_age')->example(20),
-            'roles' => Type::arrayOf(Type::string()->strict())->required()->example('admin'),
+            'roles' => Type::arrayOf(Type::string()->strict())->required()->example(['admin']),
+            'keysValues' => Type::arrayOf(Type::string()->strict())
+                ->required()
+                ->acceptStringKeys()
+                ->example(['key' => 'value']),
             'address' => Type::item([
                 'street' => Type::string()->length(15, 100)->example('Main Street'),
                 'city' => Type::string()->allowed('Paris', 'London')->example('London'),
@@ -372,6 +487,7 @@ class SchemaTest extends TestCase
             'name' => 'John Doe',
             'age' => 20,
             'roles' => ['admin'],
+            'keysValues' => ['key' => 'value'],
             'address' => [
                 'street' => 'Main Street',
                 'city' => 'London',
@@ -383,7 +499,7 @@ class SchemaTest extends TestCase
     {
 
         $schema = Schema::create([
-            'roles' => Type::arrayOf(Type::string()->strict())->required()->example('admin'),
+            'roles' => Type::arrayOf(Type::string()->strict())->required()->example(['admin']),
             'dependencies' => Type::arrayOf(Type::string()->strict())->acceptStringKeys()
         ]);
 
@@ -450,4 +566,88 @@ class SchemaTest extends TestCase
         $this->assertEquals(0, count($result->get('dependencies')));
 
     }
+
+    /**
+     * Helper to create a simple ServerRequestInterface object for tests.
+     */
+    private function createServerRequest(array $headers, array $body): ServerRequestInterface
+    {
+        return new class($headers, $body) implements ServerRequestInterface {
+            private array $headers;
+            private array $body;
+
+            public function __construct(array $headers, array $body)
+            {
+                foreach ($headers as $name => $value) {
+                    $this->headers[$name][] = $value;
+                }
+                $this->body = $body;
+            }
+
+            public function getHeaders(): array { return $this->headers; }
+            public function hasHeader($name): bool { return isset($this->headers[strtolower($name)]); }
+            public function getHeader($name): array { return (array)($this->headers[strtolower($name)] ?? []); }
+            public function getHeaderLine($name): string { return implode(', ', $this->getHeader(strtolower($name))); }
+            public function getParsedBody() { return $this->body; }
+            public function getBody(): StreamInterface {
+                $stream = $this->createMock(StreamInterface::class);
+                $stream->method('getContents')->willReturn(json_encode($this->body));
+                return $stream;
+            }
+            // --- The rest of the methods are not needed for these tests ---
+            public function getProtocolVersion(): string
+            {}
+            public function withProtocolVersion($version): \Psr\Http\Message\MessageInterface
+            {}
+            public function withHeader($name, $value): \Psr\Http\Message\MessageInterface
+            {}
+            public function withAddedHeader($name, $value): \Psr\Http\Message\MessageInterface
+            {}
+            public function withoutHeader($name): \Psr\Http\Message\MessageInterface
+            {}
+            public function withBody(StreamInterface $body): \Psr\Http\Message\MessageInterface
+            {}
+            public function getRequestTarget(): string
+            {}
+            public function withRequestTarget($requestTarget): \Psr\Http\Message\RequestInterface
+            {}
+            public function getMethod(): string
+            {}
+            public function withMethod($method): \Psr\Http\Message\RequestInterface
+            {}
+            public function getUri(): UriInterface
+            {}
+            public function withUri(UriInterface $uri, $preserveHost = false): \Psr\Http\Message\RequestInterface
+            {}
+            public function getServerParams(): array
+            {}
+            public function getCookieParams(): array
+            {}
+            public function withCookieParams(array $cookies): ServerRequestInterface
+            {}
+            public function getQueryParams(): array
+            {}
+            public function withQueryParams(array $query): ServerRequestInterface
+            {}
+            public function getUploadedFiles(): array
+            {}
+            public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface
+            {}
+            public function getAttribute($name, $default = null){}
+            public function withAttribute($name, $value): ServerRequestInterface
+            {}
+            public function withoutAttribute($name): ServerRequestInterface
+            {}
+
+            public function withParsedBody($data): ServerRequestInterface
+            {
+                // TODO: Implement withParsedBody() method.
+            }
+
+            public function getAttributes(): array
+            {
+                // TODO: Implement getAttributes() method.
+            }
+        };
+    }
 }

+ 69 - 14
tests/TypeTest.php

@@ -1,14 +1,15 @@
 <?php
-namespace Test\PhpDevCommunity\RequestKit;
-use PhpDevCommunity\RequestKit\Type\BoolType;
-use PhpDevCommunity\RequestKit\Type\DateTimeType;
-use PhpDevCommunity\RequestKit\Type\DateType;
-use PhpDevCommunity\RequestKit\Type\FloatType;
-use PhpDevCommunity\RequestKit\Type\IntType;
-use PhpDevCommunity\RequestKit\Type\NumericType;
-use PhpDevCommunity\RequestKit\Type\StringType;
-
-class TypeTest extends \PhpDevCommunity\UniTester\TestCase
+namespace Test\Depo\RequestKit;
+use Depo\RequestKit\Type;
+use Depo\RequestKit\Type\BoolType;
+use Depo\RequestKit\Type\DateTimeType;
+use Depo\RequestKit\Type\DateType;
+use Depo\RequestKit\Type\FloatType;
+use Depo\RequestKit\Type\IntType;
+use Depo\RequestKit\Type\NumericType;
+use Depo\RequestKit\Type\StringType;
+
+class TypeTest extends \Depo\UniTester\TestCase
 {
 
     protected function setUp(): void
@@ -29,8 +30,62 @@ class TypeTest extends \PhpDevCommunity\UniTester\TestCase
         $this->testDateTimeType();
         $this->testDateType();
         $this->testNumericType();
+        $this->testEqualsConstraint(); // Add new test method
     }
 
+    private function testEqualsConstraint()
+    {
+        // 1. String equals: success
+        $type = Type::string()->equals('admin');
+        $result = $type->validate('admin');
+        $this->assertTrue($result->isValid());
+        $this->assertStrictEquals('admin', $result->getValue());
+
+        // 2. String equals: failure
+        $type = Type::string()->equals('admin');
+        $result = $type->validate('user');
+        $this->assertFalse($result->isValid());
+        $this->assertEquals('The value does not match the expected value.', $result->getError());
+
+        // 3. Integer equals: success
+        $type = Type::int()->equals(123);
+        $result = $type->validate(123);
+        $this->assertTrue($result->isValid());
+        $this->assertStrictEquals(123, $result->getValue());
+
+        // 4. Integer equals: failure
+        $type = Type::int()->equals(123);
+        $result = $type->validate(456);
+        $this->assertFalse($result->isValid());
+        $this->assertEquals('The value does not match the expected value.', $result->getError());
+
+        // 5. Optional field with equals: success on null
+        $type = Type::string()->equals('secret_token')->optional();
+        $result = $type->validate(null);
+        $this->assertTrue($result->isValid());
+        $this->assertNull($result->getValue());
+
+        // 6. Required field with equals: failure on null
+        $type = Type::string()->equals('secret_token')->required();
+        $result = $type->validate(null);
+        $this->assertFalse($result->isValid());
+        $this->assertEquals('Value is required, but got null or empty string.', $result->getError());
+
+        // 7. Equals after transformation
+        $type = Type::string()->lowercase()->equals('admin');
+        $result = $type->validate('ADMIN');
+        $this->assertTrue($result->isValid());
+        $this->assertStrictEquals('admin', $result->getValue());
+
+        // 8. Security check: Ensure error message does not leak sensitive data
+        $secret = 'super_secret_api_key_12345';
+        $type = Type::string()->equals($secret);
+        $result = $type->validate('wrong_key');
+        $this->assertFalse($result->isValid());
+        $this->assertEquals('The value does not match the expected value.', $result->getError());
+    }
+
+
     private function testStringType()
     {
         $type = (new StringType())
@@ -50,13 +105,13 @@ class TypeTest extends \PhpDevCommunity\UniTester\TestCase
         $type->length(10, 20);
         $result = $type->validate("  test  ");
         $this->assertFalse($result->isValid());
-        $this->assertEquals('test must be at least 10 characters long', $result->getError());
+        $this->assertEquals('Value must be at least 10 characters long.', $result->getError());
 
 
         $type->length(1, 3);
         $result = $type->validate("  test  ");
         $this->assertFalse($result->isValid());
-        $this->assertEquals('test cannot be longer than 3 characters', $result->getError());
+        $this->assertEquals('Value cannot be longer than 3 characters.', $result->getError());
 
         $type->length(10, 20)->optional();
         $result = $type->validate(null);
@@ -66,7 +121,7 @@ class TypeTest extends \PhpDevCommunity\UniTester\TestCase
         $type->required();
         $result = $type->validate(null);
         $this->assertFalse($result->isValid());
-        $this->assertEquals('Value is required, but got null or empty string', $result->getError());
+        $this->assertEquals('Value is required, but got null or empty string.', $result->getError());
 
 
         $type->length(1);
@@ -78,7 +133,7 @@ class TypeTest extends \PhpDevCommunity\UniTester\TestCase
         $type->strict();
         $result = $type->validate(123);
         $this->assertFalse($result->isValid());
-        $this->assertEquals('Value must be a string, got: integer', $result->getError());
+        $this->assertEquals('Value must be a string, got: integer.', $result->getError());
 
 
         $type->uppercase();