Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 328 lines 11 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\DocBlockGenerator; 8use SocialDept\AtpSchema\Parser\Nsid; 9 10class DocBlockGeneratorTest extends TestCase 11{ 12 protected DocBlockGenerator $generator; 13 14 protected function setUp(): void 15 { 16 parent::setUp(); 17 18 $this->generator = new DocBlockGenerator(); 19 } 20 21 public function test_it_generates_class_docblock_with_description(): void 22 { 23 $document = $this->createDocument('app.test.post', [ 24 'type' => 'record', 25 'description' => 'A social media post', 26 'properties' => [], 27 ], 'A social media post'); 28 29 $docBlock = $this->generator->generateClassDocBlock($document, $document->getMainDefinition()); 30 31 $this->assertStringContainsString('/**', $docBlock); 32 $this->assertStringContainsString('* A social media post', $docBlock); 33 $this->assertStringContainsString('* Lexicon: app.test.post', $docBlock); 34 $this->assertStringContainsString('* Type: record', $docBlock); 35 } 36 37 public function test_it_generates_class_docblock_with_property_tags(): void 38 { 39 $document = $this->createDocument('app.test.user', [ 40 'type' => 'record', 41 'properties' => [ 42 'name' => [ 43 'type' => 'string', 44 'description' => 'User name', 45 ], 46 'age' => [ 47 'type' => 'integer', 48 ], 49 ], 50 'required' => ['name'], 51 ]); 52 53 $docBlock = $this->generator->generateClassDocBlock($document, $document->getMainDefinition()); 54 55 $this->assertStringContainsString('@property string $name User name', $docBlock); 56 $this->assertStringContainsString('@property int|null $age', $docBlock); 57 } 58 59 public function test_it_includes_validation_constraints_in_class_docblock(): void 60 { 61 $document = $this->createDocument('app.test.post', [ 62 'type' => 'record', 63 'properties' => [ 64 'text' => [ 65 'type' => 'string', 66 'maxLength' => 280, 67 ], 68 ], 69 'required' => ['text'], 70 ]); 71 72 $docBlock = $this->generator->generateClassDocBlock($document, $document->getMainDefinition()); 73 74 $this->assertStringContainsString('Constraints:', $docBlock); 75 $this->assertStringContainsString('Required: text', $docBlock); 76 $this->assertStringContainsString('text: Max length: 280', $docBlock); 77 } 78 79 public function test_it_generates_property_docblock(): void 80 { 81 $docBlock = $this->generator->generatePropertyDocBlock( 82 'title', 83 [ 84 'type' => 'string', 85 'description' => 'The post title', 86 ], 87 true 88 ); 89 90 $this->assertStringContainsString('/**', $docBlock); 91 $this->assertStringContainsString('* The post title', $docBlock); 92 $this->assertStringContainsString('* @var string', $docBlock); 93 $this->assertStringContainsString('*/', $docBlock); 94 } 95 96 public function test_it_includes_constraints_in_property_docblock(): void 97 { 98 $docBlock = $this->generator->generatePropertyDocBlock( 99 'text', 100 [ 101 'type' => 'string', 102 'maxLength' => 280, 103 'minLength' => 1, 104 ], 105 true 106 ); 107 108 $this->assertStringContainsString('@constraint Max length: 280', $docBlock); 109 $this->assertStringContainsString('@constraint Min length: 1', $docBlock); 110 } 111 112 public function test_it_generates_method_docblock(): void 113 { 114 $docBlock = $this->generator->generateMethodDocBlock( 115 'Create a new post', 116 'static', 117 [ 118 ['name' => 'text', 'type' => 'string', 'description' => 'Post text'], 119 ['name' => 'author', 'type' => 'string'], 120 ] 121 ); 122 123 $this->assertStringContainsString('* Create a new post', $docBlock); 124 $this->assertStringContainsString('* @param string $text Post text', $docBlock); 125 $this->assertStringContainsString('* @param string $author', $docBlock); 126 $this->assertStringContainsString('* @return static', $docBlock); 127 } 128 129 public function test_it_handles_void_return_type(): void 130 { 131 $docBlock = $this->generator->generateMethodDocBlock( 132 'Process data', 133 'void', 134 [] 135 ); 136 137 $this->assertStringNotContainsString('@return', $docBlock); 138 } 139 140 public function test_it_includes_throws_annotation(): void 141 { 142 $docBlock = $this->generator->generateMethodDocBlock( 143 'Validate data', 144 'bool', 145 [], 146 '\\InvalidArgumentException' 147 ); 148 149 $this->assertStringContainsString('@throws \\InvalidArgumentException', $docBlock); 150 } 151 152 public function test_it_wraps_long_descriptions(): void 153 { 154 $longDescription = 'This is a very long description that should be wrapped across multiple lines when it exceeds the maximum line width of eighty characters including the comment prefix and should definitely span more than one line'; 155 156 $docBlock = $this->generator->generatePropertyDocBlock( 157 'description', 158 [ 159 'type' => 'string', 160 'description' => $longDescription, 161 ], 162 true 163 ); 164 165 // Just verify the long description is present in the docblock 166 $this->assertStringContainsString('This is a very long description', $docBlock); 167 168 // And that it doesn't exceed reasonable line lengths 169 $lines = explode("\n", $docBlock); 170 foreach ($lines as $line) { 171 $this->assertLessThan(120, strlen($line), 'Line too long: '.$line); 172 } 173 } 174 175 public function test_it_extracts_max_length_constraint(): void 176 { 177 $constraints = $this->invokeMethod('extractPropertyConstraints', [ 178 ['maxLength' => 100], 179 ]); 180 181 $this->assertContains('@constraint Max length: 100', $constraints); 182 } 183 184 public function test_it_extracts_min_length_constraint(): void 185 { 186 $constraints = $this->invokeMethod('extractPropertyConstraints', [ 187 ['minLength' => 5], 188 ]); 189 190 $this->assertContains('@constraint Min length: 5', $constraints); 191 } 192 193 public function test_it_extracts_grapheme_constraints(): void 194 { 195 $constraints = $this->invokeMethod('extractPropertyConstraints', [ 196 ['maxGraphemes' => 280, 'minGraphemes' => 1], 197 ]); 198 199 $this->assertContains('@constraint Max graphemes: 280', $constraints); 200 $this->assertContains('@constraint Min graphemes: 1', $constraints); 201 } 202 203 public function test_it_extracts_number_constraints(): void 204 { 205 $constraints = $this->invokeMethod('extractPropertyConstraints', [ 206 ['maximum' => 100, 'minimum' => 0], 207 ]); 208 209 $this->assertContains('@constraint Maximum: 100', $constraints); 210 $this->assertContains('@constraint Minimum: 0', $constraints); 211 } 212 213 public function test_it_extracts_array_constraints(): void 214 { 215 $constraints = $this->invokeMethod('extractPropertyConstraints', [ 216 ['maxItems' => 10, 'minItems' => 1], 217 ]); 218 219 $this->assertContains('@constraint Max items: 10', $constraints); 220 $this->assertContains('@constraint Min items: 1', $constraints); 221 } 222 223 public function test_it_extracts_enum_constraint(): void 224 { 225 $constraints = $this->invokeMethod('extractPropertyConstraints', [ 226 ['enum' => ['active', 'inactive', 'pending']], 227 ]); 228 229 $this->assertContains('@constraint Enum: active, inactive, pending', $constraints); 230 } 231 232 public function test_it_extracts_format_constraint(): void 233 { 234 $constraints = $this->invokeMethod('extractPropertyConstraints', [ 235 ['format' => 'datetime'], 236 ]); 237 238 $this->assertContains('@constraint Format: datetime', $constraints); 239 } 240 241 public function test_it_extracts_const_constraint(): void 242 { 243 $constraints = $this->invokeMethod('extractPropertyConstraints', [ 244 ['const' => true], 245 ]); 246 247 $this->assertContains('@constraint Const: true', $constraints); 248 } 249 250 public function test_it_generates_simple_docblock(): void 251 { 252 $docBlock = $this->generator->generateSimple('A simple description'); 253 254 $this->assertSame(" /**\n * A simple description\n */", $docBlock); 255 } 256 257 public function test_it_generates_one_line_docblock(): void 258 { 259 $docBlock = $this->generator->generateOneLine('Quick note'); 260 261 $this->assertSame(' /** Quick note */', $docBlock); 262 } 263 264 public function test_it_handles_empty_properties(): void 265 { 266 $document = $this->createDocument('app.test.empty', [ 267 'type' => 'record', 268 'properties' => [], 269 'required' => [], 270 ]); 271 272 $docBlock = $this->generator->generateClassDocBlock($document, $document->getMainDefinition()); 273 274 $this->assertStringContainsString('Lexicon: app.test.empty', $docBlock); 275 $this->assertStringNotContainsString('@property', $docBlock); 276 } 277 278 public function test_it_handles_nullable_properties(): void 279 { 280 $document = $this->createDocument('app.test.post', [ 281 'type' => 'record', 282 'properties' => [ 283 'subtitle' => ['type' => 'string'], 284 ], 285 'required' => [], 286 ]); 287 288 $docBlock = $this->generator->generateClassDocBlock($document, $document->getMainDefinition()); 289 290 $this->assertStringContainsString('@property string|null $subtitle', $docBlock); 291 } 292 293 /** 294 * Helper to create a test document. 295 * 296 * @param array<string, mixed> $mainDef 297 */ 298 protected function createDocument(string $nsid, array $mainDef, ?string $description = null): LexiconDocument 299 { 300 return new LexiconDocument( 301 lexicon: 1, 302 id: Nsid::parse($nsid), 303 defs: ['main' => $mainDef], 304 description: $description, 305 source: null, 306 raw: [ 307 'lexicon' => 1, 308 'id' => $nsid, 309 'defs' => ['main' => $mainDef], 310 ] 311 ); 312 } 313 314 /** 315 * Helper to invoke protected method. 316 * 317 * @param array<mixed> $args 318 * @return mixed 319 */ 320 protected function invokeMethod(string $methodName, array $args) 321 { 322 $reflection = new \ReflectionClass($this->generator); 323 $method = $reflection->getMethod($methodName); 324 $method->setAccessible(true); 325 326 return $method->invokeArgs($this->generator, $args); 327 } 328}