Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Validation;
4
5class 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}