Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Tests\Integration;
4
5use Orchestra\Testbench\TestCase;
6use SocialDept\AtpSchema\Contracts\Transformer;
7use SocialDept\AtpSchema\Data\LexiconDocument;
8use SocialDept\AtpSchema\Parser\SchemaLoader;
9use SocialDept\AtpSchema\Services\ModelMapper;
10use SocialDept\AtpSchema\Validation\Validator;
11
12class ModelMappingIntegrationTest extends TestCase
13{
14 protected ModelMapper $mapper;
15
16 protected Validator $validator;
17
18 protected SchemaLoader $schemaLoader;
19
20 protected function setUp(): void
21 {
22 parent::setUp();
23
24 $this->mapper = new ModelMapper();
25 $this->schemaLoader = new SchemaLoader([]);
26 $this->validator = new Validator($this->schemaLoader);
27 }
28
29 public function test_it_transforms_and_validates_complete_workflow(): void
30 {
31 // Register transformer
32 $this->mapper->register('app.bsky.feed.post', new PostTransformer());
33
34 // Load schema
35 $schema = LexiconDocument::fromArray([
36 'lexicon' => 1,
37 'id' => 'app.bsky.feed.post',
38 'defs' => [
39 'main' => [
40 'type' => 'record',
41 'key' => 'tid',
42 'record' => [
43 'type' => 'object',
44 'required' => ['text', 'createdAt'],
45 'properties' => [
46 'text' => ['type' => 'string', 'maxLength' => 300],
47 'createdAt' => ['type' => 'string', 'format' => 'datetime'],
48 ],
49 ],
50 ],
51 ],
52 ]);
53
54 // Transform to model
55 $model = $this->mapper->fromArray('app.bsky.feed.post', [
56 'text' => 'Hello, world!',
57 'createdAt' => '2024-01-01T00:00:00Z',
58 ]);
59
60 // Verify model
61 $this->assertInstanceOf(Post::class, $model);
62 $this->assertEquals('Hello, world!', $model->text);
63
64 // Transform back to array
65 $data = $this->mapper->toArray('app.bsky.feed.post', $model);
66
67 // Validate transformed data
68 $result = $this->validator->validate($data, $schema);
69
70 $this->assertTrue($result);
71 }
72
73 public function test_it_handles_multiple_model_transformations(): void
74 {
75 $this->mapper->registerMany([
76 'app.bsky.feed.post' => new PostTransformer(),
77 'app.bsky.feed.repost' => new RepostTransformer(),
78 ]);
79
80 $posts = [
81 ['text' => 'First post', 'createdAt' => '2024-01-01T00:00:00Z'],
82 ['text' => 'Second post', 'createdAt' => '2024-01-02T00:00:00Z'],
83 ];
84
85 $models = $this->mapper->fromArrayMany('app.bsky.feed.post', $posts);
86
87 $this->assertCount(2, $models);
88 $this->assertContainsOnlyInstancesOf(Post::class, $models);
89
90 $arrays = $this->mapper->toArrayMany('app.bsky.feed.post', $models);
91
92 $this->assertEquals($posts, $arrays);
93 }
94
95 public function test_it_extends_mapper_with_macros(): void
96 {
97 ModelMapper::macro('validateAndTransform', function ($type, $data, $schema) {
98 // Validate first
99 if (! $this->validator->validate($data, $schema)) {
100 return null;
101 }
102
103 // Then transform
104 return $this->fromArray($type, $data);
105 });
106
107 $this->mapper->validator = $this->validator;
108 $this->mapper->register('app.bsky.feed.post', new PostTransformer());
109
110 $schema = LexiconDocument::fromArray([
111 'lexicon' => 1,
112 'id' => 'app.bsky.feed.post',
113 'defs' => [
114 'main' => [
115 'type' => 'record',
116 'key' => 'tid',
117 'record' => [
118 'type' => 'object',
119 'required' => ['text'],
120 'properties' => [
121 'text' => ['type' => 'string'],
122 'createdAt' => ['type' => 'string'],
123 ],
124 ],
125 ],
126 ],
127 ]);
128
129 $validData = ['text' => 'Hello', 'createdAt' => '2024-01-01T00:00:00Z'];
130
131 $result = $this->mapper->validateAndTransform('app.bsky.feed.post', $validData, $schema);
132
133 $this->assertInstanceOf(Post::class, $result);
134
135 ModelMapper::flushMacros();
136 }
137
138 public function test_it_handles_nested_transformations(): void
139 {
140 $this->mapper->register('app.bsky.actor.profile', new ProfileTransformer());
141
142 $data = [
143 'displayName' => 'Alice',
144 'description' => 'Developer',
145 'avatar' => [
146 'url' => 'https://example.com/avatar.jpg',
147 'size' => 12345,
148 ],
149 ];
150
151 $model = $this->mapper->fromArray('app.bsky.actor.profile', $data);
152
153 $this->assertInstanceOf(Profile::class, $model);
154 $this->assertEquals('Alice', $model->displayName);
155 $this->assertIsArray($model->avatar);
156
157 $transformed = $this->mapper->toArray('app.bsky.actor.profile', $model);
158
159 $this->assertEquals($data, $transformed);
160 }
161}
162
163// Test models and transformers
164class Post
165{
166 public function __construct(
167 public string $text,
168 public string $createdAt
169 ) {
170 }
171}
172
173class PostTransformer implements Transformer
174{
175 public function fromArray(array $data): Post
176 {
177 return new Post(
178 text: $data['text'],
179 createdAt: $data['createdAt']
180 );
181 }
182
183 public function toArray(mixed $model): array
184 {
185 return [
186 'text' => $model->text,
187 'createdAt' => $model->createdAt,
188 ];
189 }
190
191 public function supports(string $type): bool
192 {
193 return $type === 'app.bsky.feed.post';
194 }
195}
196
197class Repost
198{
199 public function __construct(
200 public string $uri,
201 public string $createdAt
202 ) {
203 }
204}
205
206class RepostTransformer implements Transformer
207{
208 public function fromArray(array $data): Repost
209 {
210 return new Repost(
211 uri: $data['uri'],
212 createdAt: $data['createdAt']
213 );
214 }
215
216 public function toArray(mixed $model): array
217 {
218 return [
219 'uri' => $model->uri,
220 'createdAt' => $model->createdAt,
221 ];
222 }
223
224 public function supports(string $type): bool
225 {
226 return $type === 'app.bsky.feed.repost';
227 }
228}
229
230class Profile
231{
232 public function __construct(
233 public string $displayName,
234 public string $description,
235 public array $avatar
236 ) {
237 }
238}
239
240class ProfileTransformer implements Transformer
241{
242 public function fromArray(array $data): Profile
243 {
244 return new Profile(
245 displayName: $data['displayName'],
246 description: $data['description'],
247 avatar: $data['avatar']
248 );
249 }
250
251 public function toArray(mixed $model): array
252 {
253 return [
254 'displayName' => $model->displayName,
255 'description' => $model->description,
256 'avatar' => $model->avatar,
257 ];
258 }
259
260 public function supports(string $type): bool
261 {
262 return $type === 'app.bsky.actor.profile';
263 }
264}