Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add validation error formatting

+861
+187
src/Validation/ValidationError.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation; 4 + 5 + use JsonSerializable; 6 + 7 + class ValidationError implements JsonSerializable 8 + { 9 + /** 10 + * Tracks which optional values were explicitly set. 11 + */ 12 + protected bool $hasExpectedValue = false; 13 + 14 + protected bool $hasActualValue = false; 15 + 16 + /** 17 + * Create a new ValidationError. 18 + */ 19 + public function __construct( 20 + public readonly string $field, 21 + public readonly string $message, 22 + public readonly ?string $rule = null, 23 + mixed $expected = null, 24 + mixed $actual = null, 25 + public readonly array $context = [] 26 + ) { 27 + $this->hasExpectedValue = func_num_args() >= 4; 28 + $this->hasActualValue = func_num_args() >= 5; 29 + 30 + // Use object properties for values that can be null 31 + $this->expected = $expected; 32 + $this->actual = $actual; 33 + } 34 + 35 + public readonly mixed $expected; 36 + 37 + public readonly mixed $actual; 38 + 39 + /** 40 + * Create from field and message. 41 + */ 42 + public static function make(string $field, string $message): self 43 + { 44 + return new self($field, $message); 45 + } 46 + 47 + /** 48 + * Create with rule context. 49 + */ 50 + public static function withRule(string $field, string $message, string $rule): self 51 + { 52 + return new self($field, $message, $rule); 53 + } 54 + 55 + /** 56 + * Create with full context. 57 + */ 58 + public static function withContext( 59 + string $field, 60 + string $message, 61 + string $rule, 62 + mixed $expected = null, 63 + mixed $actual = null, 64 + array $context = [] 65 + ): self { 66 + return new self($field, $message, $rule, $expected, $actual, $context); 67 + } 68 + 69 + /** 70 + * Get the field path. 71 + */ 72 + public function getField(): string 73 + { 74 + return $this->field; 75 + } 76 + 77 + /** 78 + * Get the error message. 79 + */ 80 + public function getMessage(): string 81 + { 82 + return $this->message; 83 + } 84 + 85 + /** 86 + * Get the validation rule that failed. 87 + */ 88 + public function getRule(): ?string 89 + { 90 + return $this->rule; 91 + } 92 + 93 + /** 94 + * Get the expected value. 95 + */ 96 + public function getExpected(): mixed 97 + { 98 + return $this->expected; 99 + } 100 + 101 + /** 102 + * Get the actual value. 103 + */ 104 + public function getActual(): mixed 105 + { 106 + return $this->actual; 107 + } 108 + 109 + /** 110 + * Get additional context. 111 + */ 112 + public function getContext(): array 113 + { 114 + return $this->context; 115 + } 116 + 117 + /** 118 + * Check if error has rule information. 119 + */ 120 + public function hasRule(): bool 121 + { 122 + return $this->rule !== null; 123 + } 124 + 125 + /** 126 + * Check if error has expected value. 127 + */ 128 + public function hasExpected(): bool 129 + { 130 + return $this->hasExpectedValue; 131 + } 132 + 133 + /** 134 + * Check if error has actual value. 135 + */ 136 + public function hasActual(): bool 137 + { 138 + return $this->hasActualValue; 139 + } 140 + 141 + /** 142 + * Convert to array. 143 + * 144 + * @return array<string, mixed> 145 + */ 146 + public function toArray(): array 147 + { 148 + $data = [ 149 + 'field' => $this->field, 150 + 'message' => $this->message, 151 + ]; 152 + 153 + if ($this->rule !== null) { 154 + $data['rule'] = $this->rule; 155 + } 156 + 157 + if ($this->hasExpectedValue) { 158 + $data['expected'] = $this->expected; 159 + } 160 + 161 + if ($this->hasActualValue) { 162 + $data['actual'] = $this->actual; 163 + } 164 + 165 + if (! empty($this->context)) { 166 + $data['context'] = $this->context; 167 + } 168 + 169 + return $data; 170 + } 171 + 172 + /** 173 + * Convert to JSON. 174 + */ 175 + public function jsonSerialize(): array 176 + { 177 + return $this->toArray(); 178 + } 179 + 180 + /** 181 + * Convert to string. 182 + */ 183 + public function __toString(): string 184 + { 185 + return "{$this->field}: {$this->message}"; 186 + } 187 + }
+212
src/Validation/ValidationErrorFormatter.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Validation; 4 + 5 + class ValidationErrorFormatter 6 + { 7 + /** 8 + * Format errors for Laravel ValidationException. 9 + * 10 + * @param array<ValidationError> $errors 11 + * @return array<string, array<string>> 12 + */ 13 + public function formatForLaravel(array $errors): array 14 + { 15 + $formatted = []; 16 + 17 + foreach ($errors as $error) { 18 + $field = $this->convertFieldPath($error->getField()); 19 + 20 + if (! isset($formatted[$field])) { 21 + $formatted[$field] = []; 22 + } 23 + 24 + $formatted[$field][] = $error->getMessage(); 25 + } 26 + 27 + return $formatted; 28 + } 29 + 30 + /** 31 + * Format errors as flat array of messages. 32 + * 33 + * @param array<ValidationError> $errors 34 + * @return array<string> 35 + */ 36 + public function formatAsMessages(array $errors): array 37 + { 38 + $messages = []; 39 + 40 + foreach ($errors as $error) { 41 + $messages[] = $error->getMessage(); 42 + } 43 + 44 + return $messages; 45 + } 46 + 47 + /** 48 + * Format errors with field context. 49 + * 50 + * @param array<ValidationError> $errors 51 + * @return array<string> 52 + */ 53 + public function formatWithFields(array $errors): array 54 + { 55 + $messages = []; 56 + 57 + foreach ($errors as $error) { 58 + $messages[] = $error->getField().': '.$error->getMessage(); 59 + } 60 + 61 + return $messages; 62 + } 63 + 64 + /** 65 + * Format errors as detailed array. 66 + * 67 + * @param array<ValidationError> $errors 68 + * @return array<array<string, mixed>> 69 + */ 70 + public function formatDetailed(array $errors): array 71 + { 72 + $formatted = []; 73 + 74 + foreach ($errors as $error) { 75 + $formatted[] = $error->toArray(); 76 + } 77 + 78 + return $formatted; 79 + } 80 + 81 + /** 82 + * Group errors by field. 83 + * 84 + * @param array<ValidationError> $errors 85 + * @return array<string, array<ValidationError>> 86 + */ 87 + public function groupByField(array $errors): array 88 + { 89 + $grouped = []; 90 + 91 + foreach ($errors as $error) { 92 + $field = $error->getField(); 93 + 94 + if (! isset($grouped[$field])) { 95 + $grouped[$field] = []; 96 + } 97 + 98 + $grouped[$field][] = $error; 99 + } 100 + 101 + return $grouped; 102 + } 103 + 104 + /** 105 + * Convert field path from dot notation to Laravel format. 106 + */ 107 + protected function convertFieldPath(string $path): string 108 + { 109 + // Remove leading $. if present 110 + if (str_starts_with($path, '$.')) { 111 + $path = substr($path, 2); 112 + } elseif ($path === '$') { 113 + return '_root'; 114 + } 115 + 116 + // Convert array notation from [0] to .0 117 + $path = preg_replace('/\[(\d+)\]/', '.$1', $path); 118 + 119 + return $path; 120 + } 121 + 122 + /** 123 + * Format a single error. 124 + */ 125 + public function formatError(ValidationError $error): string 126 + { 127 + $message = $error->getMessage(); 128 + 129 + if ($error->hasRule()) { 130 + $message .= " (Rule: {$error->getRule()})"; 131 + } 132 + 133 + if ($error->hasExpected() && $error->hasActual()) { 134 + $expected = $this->formatValue($error->getExpected()); 135 + $actual = $this->formatValue($error->getActual()); 136 + $message .= " [Expected: {$expected}, Got: {$actual}]"; 137 + } 138 + 139 + return $message; 140 + } 141 + 142 + /** 143 + * Format a value for display. 144 + */ 145 + protected function formatValue(mixed $value): string 146 + { 147 + if (is_null($value)) { 148 + return 'null'; 149 + } 150 + 151 + if (is_bool($value)) { 152 + return $value ? 'true' : 'false'; 153 + } 154 + 155 + if (is_array($value)) { 156 + return 'array('.count($value).')'; 157 + } 158 + 159 + if (is_object($value)) { 160 + return 'object('.get_class($value).')'; 161 + } 162 + 163 + if (is_string($value) && strlen($value) > 50) { 164 + return substr($value, 0, 50).'...'; 165 + } 166 + 167 + return (string) $value; 168 + } 169 + 170 + /** 171 + * Create human-readable summary. 172 + * 173 + * @param array<ValidationError> $errors 174 + */ 175 + public function createSummary(array $errors): string 176 + { 177 + $count = count($errors); 178 + 179 + if ($count === 0) { 180 + return 'No validation errors'; 181 + } 182 + 183 + if ($count === 1) { 184 + return 'Validation failed: '.$errors[0]->getMessage(); 185 + } 186 + 187 + $fields = array_unique(array_map(fn ($error) => $error->getField(), $errors)); 188 + $fieldCount = count($fields); 189 + 190 + return "Validation failed with {$count} errors across {$fieldCount} fields"; 191 + } 192 + 193 + /** 194 + * Format errors as JSON string. 195 + * 196 + * @param array<ValidationError> $errors 197 + */ 198 + public function toJson(array $errors, int $options = 0): string 199 + { 200 + return json_encode($this->formatDetailed($errors), $options); 201 + } 202 + 203 + /** 204 + * Format errors as pretty JSON string. 205 + * 206 + * @param array<ValidationError> $errors 207 + */ 208 + public function toPrettyJson(array $errors): string 209 + { 210 + return $this->toJson($errors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 211 + } 212 + }
+293
tests/Unit/Validation/ValidationErrorFormatterTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\ValidationError; 7 + use SocialDept\Schema\Validation\ValidationErrorFormatter; 8 + 9 + class ValidationErrorFormatterTest extends TestCase 10 + { 11 + protected ValidationErrorFormatter $formatter; 12 + 13 + protected function setUp(): void 14 + { 15 + parent::setUp(); 16 + 17 + $this->formatter = new ValidationErrorFormatter(); 18 + } 19 + 20 + public function test_it_formats_for_laravel(): void 21 + { 22 + $errors = [ 23 + ValidationError::make('name', 'Name is required'), 24 + ValidationError::make('email', 'Email is invalid'), 25 + ]; 26 + 27 + $formatted = $this->formatter->formatForLaravel($errors); 28 + 29 + $this->assertEquals([ 30 + 'name' => ['Name is required'], 31 + 'email' => ['Email is invalid'], 32 + ], $formatted); 33 + } 34 + 35 + public function test_it_groups_multiple_errors_per_field(): void 36 + { 37 + $errors = [ 38 + ValidationError::make('name', 'Name is required'), 39 + ValidationError::make('name', 'Name must be a string'), 40 + ]; 41 + 42 + $formatted = $this->formatter->formatForLaravel($errors); 43 + 44 + $this->assertEquals([ 45 + 'name' => ['Name is required', 'Name must be a string'], 46 + ], $formatted); 47 + } 48 + 49 + public function test_it_converts_field_paths(): void 50 + { 51 + $errors = [ 52 + ValidationError::make('$.user.name', 'Invalid name'), 53 + ValidationError::make('$.items[0]', 'Invalid item'), 54 + ]; 55 + 56 + $formatted = $this->formatter->formatForLaravel($errors); 57 + 58 + $this->assertArrayHasKey('user.name', $formatted); 59 + $this->assertArrayHasKey('items.0', $formatted); 60 + } 61 + 62 + public function test_it_handles_root_field(): void 63 + { 64 + $errors = [ 65 + ValidationError::make('$', 'Invalid data'), 66 + ]; 67 + 68 + $formatted = $this->formatter->formatForLaravel($errors); 69 + 70 + $this->assertArrayHasKey('_root', $formatted); 71 + } 72 + 73 + public function test_it_formats_as_messages(): void 74 + { 75 + $errors = [ 76 + ValidationError::make('name', 'Name is required'), 77 + ValidationError::make('email', 'Email is invalid'), 78 + ]; 79 + 80 + $messages = $this->formatter->formatAsMessages($errors); 81 + 82 + $this->assertEquals(['Name is required', 'Email is invalid'], $messages); 83 + } 84 + 85 + public function test_it_formats_with_fields(): void 86 + { 87 + $errors = [ 88 + ValidationError::make('name', 'Name is required'), 89 + ValidationError::make('email', 'Email is invalid'), 90 + ]; 91 + 92 + $messages = $this->formatter->formatWithFields($errors); 93 + 94 + $this->assertEquals([ 95 + 'name: Name is required', 96 + 'email: Email is invalid', 97 + ], $messages); 98 + } 99 + 100 + public function test_it_formats_detailed(): void 101 + { 102 + $errors = [ 103 + ValidationError::withContext('age', 'Too high', 'max', 100, 150), 104 + ]; 105 + 106 + $detailed = $this->formatter->formatDetailed($errors); 107 + 108 + $this->assertEquals([ 109 + [ 110 + 'field' => 'age', 111 + 'message' => 'Too high', 112 + 'rule' => 'max', 113 + 'expected' => 100, 114 + 'actual' => 150, 115 + ], 116 + ], $detailed); 117 + } 118 + 119 + public function test_it_groups_by_field(): void 120 + { 121 + $errors = [ 122 + ValidationError::make('name', 'Required'), 123 + ValidationError::make('name', 'Must be string'), 124 + ValidationError::make('email', 'Invalid'), 125 + ]; 126 + 127 + $grouped = $this->formatter->groupByField($errors); 128 + 129 + $this->assertCount(2, $grouped); 130 + $this->assertCount(2, $grouped['name']); 131 + $this->assertCount(1, $grouped['email']); 132 + } 133 + 134 + public function test_it_formats_single_error(): void 135 + { 136 + $error = ValidationError::withContext('age', 'Too high', 'max', 100, 150); 137 + 138 + $formatted = $this->formatter->formatError($error); 139 + 140 + $this->assertStringContainsString('Too high', $formatted); 141 + $this->assertStringContainsString('Rule: max', $formatted); 142 + $this->assertStringContainsString('Expected: 100', $formatted); 143 + $this->assertStringContainsString('Got: 150', $formatted); 144 + } 145 + 146 + public function test_it_formats_error_without_context(): void 147 + { 148 + $error = ValidationError::make('name', 'Required'); 149 + 150 + $formatted = $this->formatter->formatError($error); 151 + 152 + $this->assertEquals('Required', $formatted); 153 + } 154 + 155 + public function test_it_creates_summary_for_no_errors(): void 156 + { 157 + $summary = $this->formatter->createSummary([]); 158 + 159 + $this->assertEquals('No validation errors', $summary); 160 + } 161 + 162 + public function test_it_creates_summary_for_single_error(): void 163 + { 164 + $errors = [ 165 + ValidationError::make('name', 'Name is required'), 166 + ]; 167 + 168 + $summary = $this->formatter->createSummary($errors); 169 + 170 + $this->assertEquals('Validation failed: Name is required', $summary); 171 + } 172 + 173 + public function test_it_creates_summary_for_multiple_errors(): void 174 + { 175 + $errors = [ 176 + ValidationError::make('name', 'Required'), 177 + ValidationError::make('email', 'Invalid'), 178 + ]; 179 + 180 + $summary = $this->formatter->createSummary($errors); 181 + 182 + $this->assertStringContainsString('2 errors', $summary); 183 + $this->assertStringContainsString('2 fields', $summary); 184 + } 185 + 186 + public function test_it_creates_summary_for_multiple_errors_same_field(): void 187 + { 188 + $errors = [ 189 + ValidationError::make('name', 'Required'), 190 + ValidationError::make('name', 'Must be string'), 191 + ]; 192 + 193 + $summary = $this->formatter->createSummary($errors); 194 + 195 + $this->assertStringContainsString('2 errors', $summary); 196 + $this->assertStringContainsString('1 fields', $summary); 197 + } 198 + 199 + public function test_it_converts_to_json(): void 200 + { 201 + $errors = [ 202 + ValidationError::withRule('name', 'Required', 'required'), 203 + ]; 204 + 205 + $json = $this->formatter->toJson($errors); 206 + $decoded = json_decode($json, true); 207 + 208 + $this->assertCount(1, $decoded); 209 + $this->assertEquals('name', $decoded[0]['field']); 210 + $this->assertEquals('Required', $decoded[0]['message']); 211 + } 212 + 213 + public function test_it_converts_to_pretty_json(): void 214 + { 215 + $errors = [ 216 + ValidationError::make('name', 'Required'), 217 + ]; 218 + 219 + $json = $this->formatter->toPrettyJson($errors); 220 + 221 + $this->assertStringContainsString("\n", $json); 222 + $this->assertStringContainsString(' ', $json); 223 + } 224 + 225 + public function test_it_formats_null_value(): void 226 + { 227 + $error = ValidationError::withContext('field', 'message', 'type', 'string', null); 228 + 229 + $formatted = $this->formatter->formatError($error); 230 + 231 + $this->assertStringContainsString('Got: null', $formatted); 232 + } 233 + 234 + public function test_it_formats_boolean_values(): void 235 + { 236 + $error = ValidationError::withContext('field', 'message', 'type', true, false); 237 + 238 + $formatted = $this->formatter->formatError($error); 239 + 240 + $this->assertStringContainsString('Expected: true', $formatted); 241 + $this->assertStringContainsString('Got: false', $formatted); 242 + } 243 + 244 + public function test_it_formats_array_value(): void 245 + { 246 + $error = ValidationError::withContext('field', 'message', 'type', 'string', [1, 2, 3]); 247 + 248 + $formatted = $this->formatter->formatError($error); 249 + 250 + $this->assertStringContainsString('Got: array(3)', $formatted); 251 + } 252 + 253 + public function test_it_formats_long_string(): void 254 + { 255 + $longString = str_repeat('a', 100); 256 + $error = ValidationError::withContext('field', 'message', 'type', 'short', $longString); 257 + 258 + $formatted = $this->formatter->formatError($error); 259 + 260 + $this->assertStringContainsString('...', $formatted); 261 + } 262 + 263 + public function test_it_handles_empty_errors_array(): void 264 + { 265 + $formatted = $this->formatter->formatForLaravel([]); 266 + 267 + $this->assertEmpty($formatted); 268 + } 269 + 270 + public function test_it_formats_nested_field_paths(): void 271 + { 272 + $errors = [ 273 + ValidationError::make('$.user.profile.bio', 'Too long'), 274 + ]; 275 + 276 + $formatted = $this->formatter->formatForLaravel($errors); 277 + 278 + $this->assertArrayHasKey('user.profile.bio', $formatted); 279 + } 280 + 281 + public function test_it_formats_array_index_paths(): void 282 + { 283 + $errors = [ 284 + ValidationError::make('$.items[0].name', 'Required'), 285 + ValidationError::make('$.items[1].name', 'Required'), 286 + ]; 287 + 288 + $formatted = $this->formatter->formatForLaravel($errors); 289 + 290 + $this->assertArrayHasKey('items.0.name', $formatted); 291 + $this->assertArrayHasKey('items.1.name', $formatted); 292 + } 293 + }
+169
tests/Unit/Validation/ValidationErrorTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Unit\Validation; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Validation\ValidationError; 7 + 8 + class ValidationErrorTest extends TestCase 9 + { 10 + public function test_it_creates_simple_error(): void 11 + { 12 + $error = ValidationError::make('field', 'Field is required'); 13 + 14 + $this->assertEquals('field', $error->getField()); 15 + $this->assertEquals('Field is required', $error->getMessage()); 16 + $this->assertNull($error->getRule()); 17 + } 18 + 19 + public function test_it_creates_error_with_rule(): void 20 + { 21 + $error = ValidationError::withRule('field', 'Field is required', 'required'); 22 + 23 + $this->assertEquals('field', $error->getField()); 24 + $this->assertEquals('Field is required', $error->getMessage()); 25 + $this->assertEquals('required', $error->getRule()); 26 + } 27 + 28 + public function test_it_creates_error_with_full_context(): void 29 + { 30 + $error = ValidationError::withContext( 31 + 'age', 32 + 'Value exceeds maximum', 33 + 'max', 34 + 100, 35 + 150 36 + ); 37 + 38 + $this->assertEquals('age', $error->getField()); 39 + $this->assertEquals('Value exceeds maximum', $error->getMessage()); 40 + $this->assertEquals('max', $error->getRule()); 41 + $this->assertEquals(100, $error->getExpected()); 42 + $this->assertEquals(150, $error->getActual()); 43 + } 44 + 45 + public function test_it_checks_if_has_rule(): void 46 + { 47 + $withRule = ValidationError::withRule('field', 'message', 'required'); 48 + $withoutRule = ValidationError::make('field', 'message'); 49 + 50 + $this->assertTrue($withRule->hasRule()); 51 + $this->assertFalse($withoutRule->hasRule()); 52 + } 53 + 54 + public function test_it_checks_if_has_expected(): void 55 + { 56 + $withExpected = ValidationError::withContext('field', 'message', 'max', 100); 57 + $withoutExpected = ValidationError::make('field', 'message'); 58 + 59 + $this->assertTrue($withExpected->hasExpected()); 60 + $this->assertFalse($withoutExpected->hasExpected()); 61 + } 62 + 63 + public function test_it_checks_if_has_actual(): void 64 + { 65 + $withActual = ValidationError::withContext('field', 'message', 'type', 'string', 'integer'); 66 + $withoutActual = ValidationError::make('field', 'message'); 67 + 68 + $this->assertTrue($withActual->hasActual()); 69 + $this->assertFalse($withoutActual->hasActual()); 70 + } 71 + 72 + public function test_it_converts_to_array(): void 73 + { 74 + $error = ValidationError::withContext( 75 + 'field', 76 + 'message', 77 + 'max', 78 + 100, 79 + 150, 80 + ['extra' => 'data'] 81 + ); 82 + 83 + $array = $error->toArray(); 84 + 85 + $this->assertEquals('field', $array['field']); 86 + $this->assertEquals('message', $array['message']); 87 + $this->assertEquals('max', $array['rule']); 88 + $this->assertEquals(100, $array['expected']); 89 + $this->assertEquals(150, $array['actual']); 90 + $this->assertEquals(['extra' => 'data'], $array['context']); 91 + } 92 + 93 + public function test_it_converts_simple_error_to_array(): void 94 + { 95 + $error = ValidationError::make('field', 'message'); 96 + 97 + $array = $error->toArray(); 98 + 99 + $this->assertEquals(['field' => 'field', 'message' => 'message'], $array); 100 + } 101 + 102 + public function test_it_converts_to_json(): void 103 + { 104 + $error = ValidationError::withRule('field', 'message', 'required'); 105 + 106 + $json = json_encode($error); 107 + $decoded = json_decode($json, true); 108 + 109 + $this->assertEquals('field', $decoded['field']); 110 + $this->assertEquals('message', $decoded['message']); 111 + $this->assertEquals('required', $decoded['rule']); 112 + } 113 + 114 + public function test_it_converts_to_string(): void 115 + { 116 + $error = ValidationError::make('username', 'Username is required'); 117 + 118 + $this->assertEquals('username: Username is required', (string) $error); 119 + } 120 + 121 + public function test_it_stores_context_data(): void 122 + { 123 + $error = ValidationError::withContext( 124 + 'field', 125 + 'message', 126 + 'rule', 127 + null, 128 + null, 129 + ['path' => '$.user.name', 'constraint' => 'maxLength'] 130 + ); 131 + 132 + $context = $error->getContext(); 133 + 134 + $this->assertEquals('$.user.name', $context['path']); 135 + $this->assertEquals('maxLength', $context['constraint']); 136 + } 137 + 138 + public function test_it_handles_null_values(): void 139 + { 140 + $error = new ValidationError('field', 'message', null); 141 + 142 + $this->assertNull($error->getRule()); 143 + $this->assertFalse($error->hasExpected()); 144 + $this->assertFalse($error->hasActual()); 145 + $this->assertEmpty($error->getContext()); 146 + } 147 + 148 + public function test_it_handles_explicitly_null_values(): void 149 + { 150 + $error = new ValidationError('field', 'message', 'rule', null, null, []); 151 + 152 + $this->assertNull($error->getExpected()); 153 + $this->assertNull($error->getActual()); 154 + $this->assertTrue($error->hasExpected()); 155 + $this->assertTrue($error->hasActual()); 156 + } 157 + 158 + public function test_it_provides_readonly_access_to_properties(): void 159 + { 160 + $error = ValidationError::make('field', 'message'); 161 + 162 + $this->assertEquals('field', $error->field); 163 + $this->assertEquals('message', $error->message); 164 + $this->assertNull($error->rule); 165 + $this->assertNull($error->expected); 166 + $this->assertNull($error->actual); 167 + $this->assertEmpty($error->context); 168 + } 169 + }