Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Tests\Unit\Generator;
4
5use Orchestra\Testbench\TestCase;
6use SocialDept\AtpSchema\Data\LexiconDocument;
7use SocialDept\AtpSchema\Generator\MethodGenerator;
8use SocialDept\AtpSchema\Parser\Nsid;
9
10class MethodGeneratorTest extends TestCase
11{
12 protected MethodGenerator $generator;
13
14 protected function setUp(): void
15 {
16 parent::setUp();
17
18 $this->generator = new MethodGenerator();
19 }
20
21 public function test_it_generates_get_lexicon_method(): void
22 {
23 $document = $this->createDocument('app.test.post', [
24 'type' => 'record',
25 'properties' => [],
26 ]);
27
28 $method = $this->generator->generateGetLexicon($document);
29
30 $this->assertStringContainsString('public static function getLexicon(): string', $method);
31 $this->assertStringContainsString("return 'app.test.post';", $method);
32 }
33
34 public function test_it_generates_from_array_with_properties(): void
35 {
36 $document = $this->createDocument('app.test.user', [
37 'type' => 'record',
38 'record' => [
39 'properties' => [
40 'name' => ['type' => 'string'],
41 'age' => ['type' => 'integer'],
42 ],
43 'required' => ['name', 'age'],
44 ],
45 ]);
46
47 $method = $this->generator->generateFromArray($document);
48
49 $this->assertStringContainsString('public static function fromArray(array $data): static', $method);
50 $this->assertStringContainsString('return new static(', $method);
51 $this->assertStringContainsString("name: \$data['name']", $method);
52 $this->assertStringContainsString("age: \$data['age']", $method);
53 }
54
55 public function test_it_generates_from_array_with_optional_properties(): void
56 {
57 $document = $this->createDocument('app.test.user', [
58 'type' => 'record',
59 'record' => [
60 'properties' => [
61 'name' => ['type' => 'string'],
62 'nickname' => ['type' => 'string'],
63 ],
64 'required' => ['name'],
65 ],
66 ]);
67
68 $method = $this->generator->generateFromArray($document);
69
70 $this->assertStringContainsString('public static function fromArray(array $data): static', $method);
71 $this->assertStringContainsString('return new static(', $method);
72 $this->assertStringContainsString("name: \$data['name']", $method);
73 $this->assertStringContainsString("nickname: \$data['nickname'] ?? null", $method);
74 }
75
76 public function test_it_handles_ref_types_in_from_array(): void
77 {
78 $document = $this->createDocument('app.test.post', [
79 'type' => 'record',
80 'record' => [
81 'properties' => [
82 'author' => [
83 'type' => 'ref',
84 'ref' => 'app.test.author',
85 ],
86 ],
87 'required' => ['author'],
88 ],
89 ]);
90
91 $method = $this->generator->generateFromArray($document);
92
93 $this->assertStringContainsString('return new static(', $method);
94 $this->assertStringContainsString("author: Author::fromArray(\$data['author'])", $method);
95 }
96
97 public function test_it_handles_optional_ref_types(): void
98 {
99 $document = $this->createDocument('app.test.post', [
100 'type' => 'record',
101 'record' => [
102 'properties' => [
103 'author' => [
104 'type' => 'ref',
105 'ref' => 'app.test.author',
106 ],
107 ],
108 'required' => [],
109 ],
110 ]);
111
112 $method = $this->generator->generateFromArray($document);
113
114 $this->assertStringContainsString('return new static(', $method);
115 $this->assertStringContainsString("author: isset(\$data['author']) ? Author::fromArray(\$data['author']) : null", $method);
116 }
117
118 public function test_it_handles_array_of_refs(): void
119 {
120 $document = $this->createDocument('app.test.feed', [
121 'type' => 'record',
122 'record' => [
123 'properties' => [
124 'posts' => [
125 'type' => 'array',
126 'items' => [
127 'type' => 'ref',
128 'ref' => 'app.test.post',
129 ],
130 ],
131 ],
132 'required' => ['posts'],
133 ],
134 ]);
135
136 $method = $this->generator->generateFromArray($document);
137
138 $this->assertStringContainsString('return new static(', $method);
139 $this->assertStringContainsString("posts: isset(\$data['posts']) ? array_map(fn (\$item) => Post::fromArray(\$item), \$data['posts']) : []", $method);
140 }
141
142 public function test_it_generates_empty_from_array_for_no_properties(): void
143 {
144 $document = $this->createDocument('app.test.empty', [
145 'type' => 'record',
146 'properties' => [],
147 'required' => [],
148 ]);
149
150 $method = $this->generator->generateFromArray($document);
151
152 $this->assertStringContainsString('return new static();', $method);
153 }
154
155 public function test_it_generates_all_methods(): void
156 {
157 $document = $this->createDocument('app.test.post', [
158 'type' => 'record',
159 'properties' => [
160 'text' => ['type' => 'string'],
161 ],
162 'required' => ['text'],
163 ]);
164
165 $methods = $this->generator->generateAll($document);
166
167 $this->assertCount(2, $methods);
168 $this->assertStringContainsString('getLexicon', $methods[0]);
169 $this->assertStringContainsString('fromArray', $methods[1]);
170 }
171
172 public function test_it_generates_generic_method(): void
173 {
174 $method = $this->generator->generate(
175 name: 'customMethod',
176 returnType: 'string',
177 body: ' return "hello";',
178 description: 'A custom method',
179 params: [
180 ['name' => 'input', 'type' => 'string', 'description' => 'The input value'],
181 ],
182 isStatic: false
183 );
184
185 $this->assertStringContainsString('public function customMethod(string $input): string', $method);
186 $this->assertStringContainsString('* A custom method', $method);
187 $this->assertStringContainsString('* @param string $input The input value', $method);
188 $this->assertStringContainsString('* @return string', $method);
189 $this->assertStringContainsString('return "hello";', $method);
190 }
191
192 public function test_it_generates_static_method(): void
193 {
194 $method = $this->generator->generate(
195 name: 'create',
196 returnType: 'static',
197 body: ' return new static();',
198 isStatic: true
199 );
200
201 $this->assertStringContainsString('public static function create(): static', $method);
202 }
203
204 public function test_it_generates_method_with_multiple_parameters(): void
205 {
206 $method = $this->generator->generate(
207 name: 'calculate',
208 returnType: 'int',
209 body: ' return $a + $b;',
210 params: [
211 ['name' => 'a', 'type' => 'int'],
212 ['name' => 'b', 'type' => 'int'],
213 ]
214 );
215
216 $this->assertStringContainsString('function calculate(int $a, int $b): int', $method);
217 $this->assertStringContainsString('@param int $a', $method);
218 $this->assertStringContainsString('@param int $b', $method);
219 }
220
221 public function test_it_generates_method_without_return_type(): void
222 {
223 $method = $this->generator->generate(
224 name: 'doSomething',
225 returnType: '',
226 body: ' // do something',
227 );
228
229 $this->assertStringContainsString('function doSomething()', $method);
230 $this->assertStringNotContainsString('@return', $method);
231 }
232
233 public function test_it_generates_method_with_void_return(): void
234 {
235 $method = $this->generator->generate(
236 name: 'process',
237 returnType: 'void',
238 body: ' // process',
239 );
240
241 $this->assertStringContainsString('function process(): void', $method);
242 $this->assertStringNotContainsString('@return', $method);
243 }
244
245 public function test_it_handles_datetime_format(): void
246 {
247 $document = $this->createDocument('app.test.event', [
248 'type' => 'record',
249 'record' => [
250 'properties' => [
251 'createdAt' => [
252 'type' => 'string',
253 'format' => 'datetime',
254 ],
255 ],
256 'required' => ['createdAt'],
257 ],
258 ]);
259
260 $method = $this->generator->generateFromArray($document);
261
262 $this->assertStringContainsString('return new static(', $method);
263 $this->assertStringContainsString("createdAt: Carbon::parse(\$data['createdAt'])", $method);
264 }
265
266 public function test_it_handles_optional_datetime(): void
267 {
268 $document = $this->createDocument('app.test.event', [
269 'type' => 'record',
270 'record' => [
271 'properties' => [
272 'updatedAt' => [
273 'type' => 'string',
274 'format' => 'datetime',
275 ],
276 ],
277 'required' => [],
278 ],
279 ]);
280
281 $method = $this->generator->generateFromArray($document);
282
283 $this->assertStringContainsString('return new static(', $method);
284 $this->assertStringContainsString("updatedAt: isset(\$data['updatedAt']) ? Carbon::parse(\$data['updatedAt']) : null", $method);
285 }
286
287 public function test_it_handles_array_of_objects(): void
288 {
289 $document = $this->createDocument('app.test.config', [
290 'type' => 'record',
291 'record' => [
292 'properties' => [
293 'settings' => [
294 'type' => 'array',
295 'items' => [
296 'type' => 'object',
297 ],
298 ],
299 ],
300 'required' => [],
301 ],
302 ]);
303
304 $method = $this->generator->generateFromArray($document);
305
306 $this->assertStringContainsString('return new static(', $method);
307 $this->assertStringContainsString("settings: \$data['settings'] ?? []", $method);
308 }
309
310 public function test_it_does_not_add_trailing_comma_to_last_assignment(): void
311 {
312 $document = $this->createDocument('app.test.user', [
313 'type' => 'record',
314 'record' => [
315 'properties' => [
316 'first' => ['type' => 'string'],
317 'last' => ['type' => 'string'],
318 ],
319 'required' => ['first', 'last'],
320 ],
321 ]);
322
323 $method = $this->generator->generateFromArray($document);
324
325 $this->assertStringContainsString('return new static(', $method);
326 $this->assertStringContainsString("first: \$data['first'],", $method);
327 $this->assertStringNotContainsString("last: \$data['last'],", $method);
328 $this->assertStringContainsString("last: \$data['last']", $method);
329 }
330
331 public function test_it_includes_method_docblocks(): void
332 {
333 $document = $this->createDocument('app.test.post', [
334 'type' => 'record',
335 'properties' => [
336 'text' => ['type' => 'string'],
337 ],
338 'required' => ['text'],
339 ]);
340
341 $method = $this->generator->generateFromArray($document);
342
343 $this->assertStringContainsString('/**', $method);
344 $this->assertStringContainsString('* Create an instance from an array.', $method);
345 $this->assertStringContainsString('* @param array $data', $method);
346 $this->assertStringContainsString('* @return static', $method);
347 $this->assertStringContainsString('*/', $method);
348 }
349
350 public function test_it_generates_to_model_method(): void
351 {
352 $method = $this->generator->generateToModel([
353 'name' => ['type' => 'string'],
354 'age' => ['type' => 'integer'],
355 ], 'User');
356
357 $this->assertStringContainsString('public function toModel(): User', $method);
358 $this->assertStringContainsString('* Convert to a Laravel model instance.', $method);
359 $this->assertStringContainsString('return new User([', $method);
360 $this->assertStringContainsString("'name' => \$this->name,", $method);
361 $this->assertStringContainsString("'age' => \$this->age,", $method);
362 }
363
364 public function test_it_generates_from_model_method(): void
365 {
366 $method = $this->generator->generateFromModel([
367 'name' => ['type' => 'string'],
368 'age' => ['type' => 'integer'],
369 ], 'User');
370
371 $this->assertStringContainsString('public static function fromModel(User $model): static', $method);
372 $this->assertStringContainsString('* Create an instance from a Laravel model.', $method);
373 $this->assertStringContainsString('return new static(', $method);
374 $this->assertStringContainsString('name: $model->name ?? null,', $method);
375 $this->assertStringContainsString('age: $model->age ?? null', $method);
376 }
377
378 public function test_it_gets_model_mapper(): void
379 {
380 $mapper = $this->generator->getModelMapper();
381
382 $this->assertInstanceOf(\SocialDept\AtpSchema\Generator\ModelMapper::class, $mapper);
383 }
384
385 /**
386 * Helper to create a test document.
387 *
388 * @param array<string, mixed> $mainDef
389 */
390 protected function createDocument(string $nsid, array $mainDef): LexiconDocument
391 {
392 return new LexiconDocument(
393 lexicon: 1,
394 id: Nsid::parse($nsid),
395 defs: ['main' => $mainDef],
396 description: null,
397 source: null,
398 raw: [
399 'lexicon' => 1,
400 'id' => $nsid,
401 'defs' => ['main' => $mainDef],
402 ]
403 );
404 }
405}