Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 405 lines 14 kB view raw
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}