Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Tests\Unit\Data;
4
5use Orchestra\Testbench\TestCase;
6use SocialDept\AtpSchema\Data\LexiconDocument;
7use SocialDept\AtpSchema\Exceptions\SchemaValidationException;
8use SocialDept\AtpSchema\Parser\Nsid;
9
10class LexiconDocumentTest extends TestCase
11{
12 public function test_it_creates_from_array(): void
13 {
14 $data = [
15 'lexicon' => 1,
16 'id' => 'app.bsky.feed.post',
17 'description' => 'A post record',
18 'defs' => [
19 'main' => [
20 'type' => 'record',
21 ],
22 ],
23 ];
24
25 $doc = LexiconDocument::fromArray($data, 'test.json');
26
27 $this->assertSame(1, $doc->lexicon);
28 $this->assertInstanceOf(Nsid::class, $doc->id);
29 $this->assertSame('app.bsky.feed.post', $doc->id->toString());
30 $this->assertSame('A post record', $doc->description);
31 $this->assertArrayHasKey('main', $doc->defs);
32 $this->assertSame('test.json', $doc->source);
33 $this->assertSame($data, $doc->raw);
34 }
35
36 public function test_it_creates_from_json(): void
37 {
38 $json = json_encode([
39 'lexicon' => 1,
40 'id' => 'app.bsky.feed.post',
41 'description' => 'A post record',
42 'defs' => [
43 'main' => [
44 'type' => 'record',
45 ],
46 ],
47 ]);
48
49 $doc = LexiconDocument::fromJson($json, 'test.json');
50
51 $this->assertSame(1, $doc->lexicon);
52 $this->assertInstanceOf(Nsid::class, $doc->id);
53 $this->assertSame('app.bsky.feed.post', $doc->id->toString());
54 $this->assertSame('A post record', $doc->description);
55 $this->assertArrayHasKey('main', $doc->defs);
56 $this->assertSame('test.json', $doc->source);
57 }
58
59 public function test_it_creates_from_json_without_source(): void
60 {
61 $json = json_encode([
62 'lexicon' => 1,
63 'id' => 'app.bsky.feed.post',
64 'defs' => [
65 'main' => ['type' => 'record'],
66 ],
67 ]);
68
69 $doc = LexiconDocument::fromJson($json);
70
71 $this->assertSame(1, $doc->lexicon);
72 $this->assertSame('app.bsky.feed.post', $doc->id->toString());
73 $this->assertNull($doc->source);
74 }
75
76 public function test_it_throws_on_invalid_json(): void
77 {
78 $this->expectException(\InvalidArgumentException::class);
79 $this->expectExceptionMessage('Invalid JSON');
80
81 LexiconDocument::fromJson('{"invalid json');
82 }
83
84 public function test_it_throws_on_non_array_json(): void
85 {
86 $this->expectException(\InvalidArgumentException::class);
87 $this->expectExceptionMessage('JSON must decode to an array');
88
89 LexiconDocument::fromJson('"just a string"');
90 }
91
92 public function test_it_throws_on_json_array_list(): void
93 {
94 $this->expectException(\InvalidArgumentException::class);
95 $this->expectExceptionMessage('JSON must decode to an array');
96
97 LexiconDocument::fromJson('123');
98 }
99
100 public function test_it_throws_on_missing_lexicon(): void
101 {
102 $this->expectException(SchemaValidationException::class);
103 $this->expectExceptionMessage('Required field missing in schema unknown: lexicon');
104
105 LexiconDocument::fromArray([
106 'id' => 'app.bsky.feed.post',
107 'defs' => [],
108 ]);
109 }
110
111 public function test_it_throws_on_missing_id(): void
112 {
113 $this->expectException(SchemaValidationException::class);
114 $this->expectExceptionMessage('Required field missing in schema unknown: id');
115
116 LexiconDocument::fromArray([
117 'lexicon' => 1,
118 'defs' => [],
119 ]);
120 }
121
122 public function test_it_throws_on_missing_defs(): void
123 {
124 $this->expectException(SchemaValidationException::class);
125 $this->expectExceptionMessage('Required field missing in schema app.bsky.feed.post: defs');
126
127 LexiconDocument::fromArray([
128 'lexicon' => 1,
129 'id' => 'app.bsky.feed.post',
130 ]);
131 }
132
133 public function test_it_throws_on_invalid_lexicon_version(): void
134 {
135 $this->expectException(SchemaValidationException::class);
136 $this->expectExceptionMessage('Unsupported lexicon version 2 in schema app.bsky.feed.post');
137
138 LexiconDocument::fromArray([
139 'lexicon' => 2,
140 'id' => 'app.bsky.feed.post',
141 'defs' => [],
142 ]);
143 }
144
145 public function test_it_gets_definition(): void
146 {
147 $doc = LexiconDocument::fromArray([
148 'lexicon' => 1,
149 'id' => 'app.bsky.feed.post',
150 'defs' => [
151 'main' => ['type' => 'record'],
152 'other' => ['type' => 'object'],
153 ],
154 ]);
155
156 $this->assertSame(['type' => 'record'], $doc->getDefinition('main'));
157 $this->assertSame(['type' => 'object'], $doc->getDefinition('other'));
158 $this->assertNull($doc->getDefinition('nonexistent'));
159 }
160
161 public function test_it_checks_definition_exists(): void
162 {
163 $doc = LexiconDocument::fromArray([
164 'lexicon' => 1,
165 'id' => 'app.bsky.feed.post',
166 'defs' => [
167 'main' => ['type' => 'record'],
168 ],
169 ]);
170
171 $this->assertTrue($doc->hasDefinition('main'));
172 $this->assertFalse($doc->hasDefinition('nonexistent'));
173 }
174
175 public function test_it_gets_main_definition(): void
176 {
177 $doc = LexiconDocument::fromArray([
178 'lexicon' => 1,
179 'id' => 'app.bsky.feed.post',
180 'defs' => [
181 'main' => ['type' => 'record'],
182 ],
183 ]);
184
185 $this->assertSame(['type' => 'record'], $doc->getMainDefinition());
186 }
187
188 public function test_it_gets_definition_names(): void
189 {
190 $doc = LexiconDocument::fromArray([
191 'lexicon' => 1,
192 'id' => 'app.bsky.feed.post',
193 'defs' => [
194 'main' => ['type' => 'record'],
195 'other' => ['type' => 'object'],
196 'another' => ['type' => 'string'],
197 ],
198 ]);
199
200 $names = $doc->getDefinitionNames();
201
202 $this->assertCount(3, $names);
203 $this->assertContains('main', $names);
204 $this->assertContains('other', $names);
205 $this->assertContains('another', $names);
206 }
207
208 public function test_it_gets_nsid(): void
209 {
210 $doc = LexiconDocument::fromArray([
211 'lexicon' => 1,
212 'id' => 'app.bsky.feed.post',
213 'defs' => [],
214 ]);
215
216 $this->assertSame('app.bsky.feed.post', $doc->getNsid());
217 }
218
219 public function test_it_gets_version(): void
220 {
221 $doc = LexiconDocument::fromArray([
222 'lexicon' => 1,
223 'id' => 'app.bsky.feed.post',
224 'defs' => [],
225 ]);
226
227 $this->assertSame(1, $doc->getVersion());
228 }
229
230 public function test_it_gets_version_same_as_lexicon_property(): void
231 {
232 $doc = LexiconDocument::fromArray([
233 'lexicon' => 1,
234 'id' => 'app.bsky.feed.post',
235 'defs' => [],
236 ]);
237
238 $this->assertSame($doc->lexicon, $doc->getVersion());
239 }
240
241 public function test_it_converts_to_array(): void
242 {
243 $doc = LexiconDocument::fromArray([
244 'lexicon' => 1,
245 'id' => 'app.bsky.feed.post',
246 'description' => 'A post record',
247 'defs' => [
248 'main' => ['type' => 'record'],
249 ],
250 ]);
251
252 $array = $doc->toArray();
253
254 $this->assertSame(1, $array['lexicon']);
255 $this->assertSame('app.bsky.feed.post', $array['id']);
256 $this->assertSame('A post record', $array['description']);
257 $this->assertArrayHasKey('main', $array['defs']);
258 }
259
260 public function test_it_identifies_record_schema(): void
261 {
262 $doc = LexiconDocument::fromArray([
263 'lexicon' => 1,
264 'id' => 'app.bsky.feed.post',
265 'defs' => [
266 'main' => ['type' => 'record'],
267 ],
268 ]);
269
270 $this->assertTrue($doc->isRecord());
271 $this->assertFalse($doc->isQuery());
272 $this->assertFalse($doc->isProcedure());
273 $this->assertFalse($doc->isSubscription());
274 }
275
276 public function test_it_identifies_query_schema(): void
277 {
278 $doc = LexiconDocument::fromArray([
279 'lexicon' => 1,
280 'id' => 'com.atproto.repo.getRecord',
281 'defs' => [
282 'main' => ['type' => 'query'],
283 ],
284 ]);
285
286 $this->assertTrue($doc->isQuery());
287 $this->assertFalse($doc->isRecord());
288 $this->assertFalse($doc->isProcedure());
289 $this->assertFalse($doc->isSubscription());
290 }
291
292 public function test_it_identifies_procedure_schema(): void
293 {
294 $doc = LexiconDocument::fromArray([
295 'lexicon' => 1,
296 'id' => 'com.atproto.repo.createRecord',
297 'defs' => [
298 'main' => ['type' => 'procedure'],
299 ],
300 ]);
301
302 $this->assertTrue($doc->isProcedure());
303 $this->assertFalse($doc->isRecord());
304 $this->assertFalse($doc->isQuery());
305 $this->assertFalse($doc->isSubscription());
306 }
307
308 public function test_it_identifies_subscription_schema(): void
309 {
310 $doc = LexiconDocument::fromArray([
311 'lexicon' => 1,
312 'id' => 'com.atproto.sync.subscribeRepos',
313 'defs' => [
314 'main' => ['type' => 'subscription'],
315 ],
316 ]);
317
318 $this->assertTrue($doc->isSubscription());
319 $this->assertFalse($doc->isRecord());
320 $this->assertFalse($doc->isQuery());
321 $this->assertFalse($doc->isProcedure());
322 }
323}