Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Tests\Unit\Services;
4
5use Orchestra\Testbench\TestCase;
6use SocialDept\AtpSchema\Contracts\Transformer;
7use SocialDept\AtpSchema\Exceptions\SchemaException;
8use SocialDept\AtpSchema\Services\ModelMapper;
9
10class ModelMapperTest extends TestCase
11{
12 protected ModelMapper $mapper;
13
14 protected function setUp(): void
15 {
16 parent::setUp();
17
18 $this->mapper = new ModelMapper();
19 }
20
21 public function test_it_registers_transformer(): void
22 {
23 $transformer = $this->createTestTransformer('app.bsky.feed.post');
24
25 $this->mapper->register('app.bsky.feed.post', $transformer);
26
27 $this->assertTrue($this->mapper->has('app.bsky.feed.post'));
28 }
29
30 public function test_it_registers_multiple_transformers(): void
31 {
32 $transformer1 = $this->createTestTransformer('app.bsky.feed.post');
33 $transformer2 = $this->createTestTransformer('app.bsky.feed.repost');
34
35 $this->mapper->registerMany([
36 'app.bsky.feed.post' => $transformer1,
37 'app.bsky.feed.repost' => $transformer2,
38 ]);
39
40 $this->assertTrue($this->mapper->has('app.bsky.feed.post'));
41 $this->assertTrue($this->mapper->has('app.bsky.feed.repost'));
42 }
43
44 public function test_it_transforms_from_array(): void
45 {
46 $transformer = $this->createTestTransformer('app.bsky.feed.post');
47 $this->mapper->register('app.bsky.feed.post', $transformer);
48
49 $data = ['text' => 'Hello, world!'];
50 $model = $this->mapper->fromArray('app.bsky.feed.post', $data);
51
52 $this->assertInstanceOf(\stdClass::class, $model);
53 $this->assertEquals('Hello, world!', $model->text);
54 }
55
56 public function test_it_transforms_to_array(): void
57 {
58 $transformer = $this->createTestTransformer('app.bsky.feed.post');
59 $this->mapper->register('app.bsky.feed.post', $transformer);
60
61 $model = (object) ['text' => 'Hello, world!'];
62 $data = $this->mapper->toArray('app.bsky.feed.post', $model);
63
64 $this->assertEquals(['text' => 'Hello, world!'], $data);
65 }
66
67 public function test_it_throws_exception_when_transformer_not_found_for_from_array(): void
68 {
69 $this->expectException(SchemaException::class);
70
71 $this->mapper->fromArray('unknown.type', []);
72 }
73
74 public function test_it_throws_exception_when_transformer_not_found_for_to_array(): void
75 {
76 $this->expectException(SchemaException::class);
77
78 $this->mapper->toArray('unknown.type', new \stdClass());
79 }
80
81 public function test_it_transforms_multiple_items_from_arrays(): void
82 {
83 $transformer = $this->createTestTransformer('app.bsky.feed.post');
84 $this->mapper->register('app.bsky.feed.post', $transformer);
85
86 $items = [
87 ['text' => 'First post'],
88 ['text' => 'Second post'],
89 ];
90
91 $models = $this->mapper->fromArrayMany('app.bsky.feed.post', $items);
92
93 $this->assertCount(2, $models);
94 $this->assertEquals('First post', $models[0]->text);
95 $this->assertEquals('Second post', $models[1]->text);
96 }
97
98 public function test_it_transforms_multiple_items_to_arrays(): void
99 {
100 $transformer = $this->createTestTransformer('app.bsky.feed.post');
101 $this->mapper->register('app.bsky.feed.post', $transformer);
102
103 $models = [
104 (object) ['text' => 'First post'],
105 (object) ['text' => 'Second post'],
106 ];
107
108 $arrays = $this->mapper->toArrayMany('app.bsky.feed.post', $models);
109
110 $this->assertCount(2, $arrays);
111 $this->assertEquals(['text' => 'First post'], $arrays[0]);
112 $this->assertEquals(['text' => 'Second post'], $arrays[1]);
113 }
114
115 public function test_it_gets_transformer(): void
116 {
117 $transformer = $this->createTestTransformer('app.bsky.feed.post');
118 $this->mapper->register('app.bsky.feed.post', $transformer);
119
120 $retrieved = $this->mapper->getTransformer('app.bsky.feed.post');
121
122 $this->assertSame($transformer, $retrieved);
123 }
124
125 public function test_it_returns_null_for_missing_transformer(): void
126 {
127 $transformer = $this->mapper->getTransformer('unknown.type');
128
129 $this->assertNull($transformer);
130 }
131
132 public function test_it_checks_if_has_transformer(): void
133 {
134 $transformer = $this->createTestTransformer('app.bsky.feed.post');
135 $this->mapper->register('app.bsky.feed.post', $transformer);
136
137 $this->assertTrue($this->mapper->has('app.bsky.feed.post'));
138 $this->assertFalse($this->mapper->has('unknown.type'));
139 }
140
141 public function test_it_unregisters_transformer(): void
142 {
143 $transformer = $this->createTestTransformer('app.bsky.feed.post');
144 $this->mapper->register('app.bsky.feed.post', $transformer);
145
146 $this->assertTrue($this->mapper->has('app.bsky.feed.post'));
147
148 $this->mapper->unregister('app.bsky.feed.post');
149
150 $this->assertFalse($this->mapper->has('app.bsky.feed.post'));
151 }
152
153 public function test_it_gets_all_transformers(): void
154 {
155 $transformer1 = $this->createTestTransformer('app.bsky.feed.post');
156 $transformer2 = $this->createTestTransformer('app.bsky.feed.repost');
157
158 $this->mapper->registerMany([
159 'app.bsky.feed.post' => $transformer1,
160 'app.bsky.feed.repost' => $transformer2,
161 ]);
162
163 $all = $this->mapper->all();
164
165 $this->assertCount(2, $all);
166 $this->assertArrayHasKey('app.bsky.feed.post', $all);
167 $this->assertArrayHasKey('app.bsky.feed.repost', $all);
168 }
169
170 public function test_it_clears_all_transformers(): void
171 {
172 $transformer = $this->createTestTransformer('app.bsky.feed.post');
173 $this->mapper->register('app.bsky.feed.post', $transformer);
174
175 $this->assertEquals(1, $this->mapper->count());
176
177 $this->mapper->clear();
178
179 $this->assertEquals(0, $this->mapper->count());
180 }
181
182 public function test_it_tries_from_array_with_missing_transformer(): void
183 {
184 $result = $this->mapper->tryFromArray('unknown.type', []);
185
186 $this->assertNull($result);
187 }
188
189 public function test_it_tries_from_array_with_existing_transformer(): void
190 {
191 $transformer = $this->createTestTransformer('app.bsky.feed.post');
192 $this->mapper->register('app.bsky.feed.post', $transformer);
193
194 $result = $this->mapper->tryFromArray('app.bsky.feed.post', ['text' => 'Hello']);
195
196 $this->assertNotNull($result);
197 $this->assertEquals('Hello', $result->text);
198 }
199
200 public function test_it_tries_to_array_with_missing_transformer(): void
201 {
202 $result = $this->mapper->tryToArray('unknown.type', new \stdClass());
203
204 $this->assertNull($result);
205 }
206
207 public function test_it_tries_to_array_with_existing_transformer(): void
208 {
209 $transformer = $this->createTestTransformer('app.bsky.feed.post');
210 $this->mapper->register('app.bsky.feed.post', $transformer);
211
212 $model = (object) ['text' => 'Hello'];
213 $result = $this->mapper->tryToArray('app.bsky.feed.post', $model);
214
215 $this->assertNotNull($result);
216 $this->assertEquals(['text' => 'Hello'], $result);
217 }
218
219 public function test_it_counts_transformers(): void
220 {
221 $this->assertEquals(0, $this->mapper->count());
222
223 $this->mapper->register('app.bsky.feed.post', $this->createTestTransformer('app.bsky.feed.post'));
224
225 $this->assertEquals(1, $this->mapper->count());
226
227 $this->mapper->register('app.bsky.feed.repost', $this->createTestTransformer('app.bsky.feed.repost'));
228
229 $this->assertEquals(2, $this->mapper->count());
230 }
231
232 public function test_it_uses_wildcard_transformer(): void
233 {
234 $transformer = $this->createWildcardTransformer('app.bsky.feed.*');
235 $this->mapper->register('app.bsky.feed.*', $transformer);
236
237 $this->assertTrue($this->mapper->has('app.bsky.feed.post'));
238 $this->assertTrue($this->mapper->has('app.bsky.feed.repost'));
239 $this->assertFalse($this->mapper->has('app.bsky.graph.follow'));
240 }
241
242 public function test_it_prefers_exact_match_over_wildcard(): void
243 {
244 $wildcardTransformer = $this->createWildcardTransformer('app.bsky.feed.*');
245 $exactTransformer = $this->createTestTransformer('app.bsky.feed.post');
246
247 $this->mapper->register('app.bsky.feed.*', $wildcardTransformer);
248 $this->mapper->register('app.bsky.feed.post', $exactTransformer);
249
250 $retrieved = $this->mapper->getTransformer('app.bsky.feed.post');
251
252 $this->assertSame($exactTransformer, $retrieved);
253 }
254
255 public function test_it_chains_register_calls(): void
256 {
257 $transformer1 = $this->createTestTransformer('type1');
258 $transformer2 = $this->createTestTransformer('type2');
259
260 $result = $this->mapper
261 ->register('type1', $transformer1)
262 ->register('type2', $transformer2);
263
264 $this->assertSame($this->mapper, $result);
265 $this->assertTrue($this->mapper->has('type1'));
266 $this->assertTrue($this->mapper->has('type2'));
267 }
268
269 public function test_it_chains_register_many_calls(): void
270 {
271 $result = $this->mapper->registerMany([
272 'type1' => $this->createTestTransformer('type1'),
273 'type2' => $this->createTestTransformer('type2'),
274 ]);
275
276 $this->assertSame($this->mapper, $result);
277 }
278
279 public function test_it_chains_unregister_calls(): void
280 {
281 $this->mapper->register('type1', $this->createTestTransformer('type1'));
282
283 $result = $this->mapper->unregister('type1');
284
285 $this->assertSame($this->mapper, $result);
286 }
287
288 public function test_it_chains_clear_calls(): void
289 {
290 $result = $this->mapper->clear();
291
292 $this->assertSame($this->mapper, $result);
293 }
294
295 protected function createTestTransformer(string $type): Transformer
296 {
297 return new class ($type) implements Transformer {
298 public function __construct(protected string $type)
299 {
300 }
301
302 public function fromArray(array $data): mixed
303 {
304 return (object) $data;
305 }
306
307 public function toArray(mixed $model): array
308 {
309 return (array) $model;
310 }
311
312 public function supports(string $type): bool
313 {
314 return $type === $this->type;
315 }
316 };
317 }
318
319 protected function createWildcardTransformer(string $pattern): Transformer
320 {
321 return new class ($pattern) implements Transformer {
322 public function __construct(protected string $pattern)
323 {
324 }
325
326 public function fromArray(array $data): mixed
327 {
328 return (object) $data;
329 }
330
331 public function toArray(mixed $model): array
332 {
333 return (array) $model;
334 }
335
336 public function supports(string $type): bool
337 {
338 $regex = '/^'.str_replace('\\*', '.*', preg_quote($this->pattern, '/')).'$/';
339
340 return (bool) preg_match($regex, $type);
341 }
342 };
343 }
344}