Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Tests\Unit\Parser;
4
5use Orchestra\Testbench\TestCase;
6use SocialDept\AtpSchema\Data\LexiconDocument;
7use SocialDept\AtpSchema\Data\Types\ObjectType;
8use SocialDept\AtpSchema\Data\Types\StringType;
9use SocialDept\AtpSchema\Exceptions\TypeResolutionException;
10use SocialDept\AtpSchema\Parser\TypeParser;
11
12class TypeParserTest extends TestCase
13{
14 protected TypeParser $parser;
15
16 protected function setUp(): void
17 {
18 parent::setUp();
19
20 $this->parser = new TypeParser();
21 }
22
23 public function test_it_parses_primitive_types(): void
24 {
25 $type = $this->parser->parse(['type' => 'string']);
26
27 $this->assertInstanceOf(StringType::class, $type);
28 }
29
30 public function test_it_parses_complex_types(): void
31 {
32 $type = $this->parser->parse(['type' => 'object']);
33
34 $this->assertInstanceOf(ObjectType::class, $type);
35 }
36
37 public function test_it_throws_on_missing_type(): void
38 {
39 $this->expectException(TypeResolutionException::class);
40 $this->expectExceptionMessage('Unknown Lexicon type: (missing type field)');
41
42 $this->parser->parse([]);
43 }
44
45 public function test_it_throws_on_unknown_type(): void
46 {
47 $this->expectException(TypeResolutionException::class);
48 $this->expectExceptionMessage('Unknown Lexicon type: nonexistent');
49
50 $this->parser->parse(['type' => 'nonexistent']);
51 }
52
53 public function test_it_resolves_local_reference(): void
54 {
55 $document = LexiconDocument::fromArray([
56 'lexicon' => 1,
57 'id' => 'com.example.test',
58 'defs' => [
59 'main' => ['type' => 'object'],
60 'other' => ['type' => 'string'],
61 ],
62 ]);
63
64 $type = $this->parser->resolveReference('#other', $document);
65
66 $this->assertInstanceOf(StringType::class, $type);
67 }
68
69 public function test_it_throws_on_unresolvable_local_reference(): void
70 {
71 $document = LexiconDocument::fromArray([
72 'lexicon' => 1,
73 'id' => 'com.example.test',
74 'defs' => [
75 'main' => ['type' => 'object'],
76 ],
77 ]);
78
79 $this->expectException(TypeResolutionException::class);
80 $this->expectExceptionMessage('Cannot resolve reference #nonexistent in schema com.example.test');
81
82 $this->parser->resolveReference('#nonexistent', $document);
83 }
84
85 public function test_it_caches_resolved_types(): void
86 {
87 $document = LexiconDocument::fromArray([
88 'lexicon' => 1,
89 'id' => 'com.example.test',
90 'defs' => [
91 'main' => ['type' => 'object'],
92 'other' => ['type' => 'string'],
93 ],
94 ]);
95
96 $type1 = $this->parser->resolveReference('#other', $document);
97 $type2 = $this->parser->resolveReference('#other', $document);
98
99 $this->assertSame($type1, $type2);
100 $this->assertCount(1, $this->parser->getResolvedTypes());
101 }
102
103 public function test_it_clears_cache(): void
104 {
105 $document = LexiconDocument::fromArray([
106 'lexicon' => 1,
107 'id' => 'com.example.test',
108 'defs' => [
109 'main' => ['type' => 'object'],
110 'other' => ['type' => 'string'],
111 ],
112 ]);
113
114 $this->parser->resolveReference('#other', $document);
115 $this->assertCount(1, $this->parser->getResolvedTypes());
116
117 $this->parser->clearCache();
118 $this->assertCount(0, $this->parser->getResolvedTypes());
119 }
120
121 public function test_it_throws_on_external_reference_without_loader(): void
122 {
123 $document = LexiconDocument::fromArray([
124 'lexicon' => 1,
125 'id' => 'com.example.test',
126 'defs' => [
127 'main' => ['type' => 'object'],
128 ],
129 ]);
130
131 $this->expectException(\RuntimeException::class);
132 $this->expectExceptionMessage('Cannot resolve external reference without SchemaLoader');
133
134 $this->parser->resolveReference('com.atproto.repo.getRecord', $document);
135 }
136}