Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 9.0 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Generator; 4 5use SocialDept\AtpSchema\Data\LexiconDocument; 6 7class DocBlockGenerator 8{ 9 /** 10 * Type mapper instance. 11 */ 12 protected TypeMapper $typeMapper; 13 14 /** 15 * Create a new DocBlockGenerator. 16 */ 17 public function __construct(?TypeMapper $typeMapper = null) 18 { 19 $this->typeMapper = $typeMapper ?? new TypeMapper(); 20 } 21 22 /** 23 * Generate a class-level docblock with rich annotations. 24 * 25 * @param array<string, mixed> $definition 26 */ 27 public function generateClassDocBlock( 28 LexiconDocument $document, 29 array $definition 30 ): string { 31 $lines = ['/**']; 32 33 // Add generated code warning 34 $lines[] = ' * GENERATED CODE - DO NOT EDIT'; 35 $lines[] = ' *'; 36 37 // Add description 38 if ($document->description) { 39 $lines = array_merge($lines, $this->wrapDescription($document->description)); 40 $lines[] = ' *'; 41 } 42 43 // Add lexicon metadata 44 $lines[] = ' * Lexicon: '.$document->getNsid(); 45 46 if (isset($definition['type'])) { 47 $lines[] = ' * Type: '.$definition['type']; 48 } 49 50 // Add @property tags for magic access 51 $properties = $definition['properties'] ?? []; 52 $required = $definition['required'] ?? []; 53 54 if (! empty($properties)) { 55 $lines[] = ' *'; 56 foreach ($properties as $name => $propDef) { 57 $isRequired = in_array($name, $required); 58 $docType = $this->typeMapper->toPhpDocType($propDef, ! $isRequired); 59 $desc = $propDef['description'] ?? ''; 60 61 if ($desc) { 62 $lines[] = ' * @property '.$docType.' $'.$name.' '.$desc; 63 } else { 64 $lines[] = ' * @property '.$docType.' $'.$name; 65 } 66 } 67 } 68 69 // Add validation constraints as annotations 70 if (! empty($properties)) { 71 $constraints = $this->extractConstraints($properties, $required); 72 if (! empty($constraints)) { 73 $lines[] = ' *'; 74 $lines[] = ' * Constraints:'; 75 foreach ($constraints as $constraint) { 76 $lines[] = ' * - '.$constraint; 77 } 78 } 79 } 80 81 $lines[] = ' */'; 82 83 return implode("\n", $lines); 84 } 85 86 /** 87 * Generate a property-level docblock. 88 * 89 * @param array<string, mixed> $definition 90 */ 91 public function generatePropertyDocBlock( 92 string $name, 93 array $definition, 94 bool $isRequired 95 ): string { 96 $lines = [' /**']; 97 98 // Add description 99 if (isset($definition['description'])) { 100 $lines = array_merge($lines, $this->wrapDescription($definition['description'], ' * ')); 101 $lines[] = ' *'; 102 } 103 104 // Add type annotation 105 $docType = $this->typeMapper->toPhpDocType($definition, ! $isRequired); 106 $lines[] = ' * @var '.$docType; 107 108 // Add validation constraints 109 $constraints = $this->extractPropertyConstraints($definition); 110 if (! empty($constraints)) { 111 $lines[] = ' *'; 112 foreach ($constraints as $constraint) { 113 $lines[] = ' * '.$constraint; 114 } 115 } 116 117 $lines[] = ' */'; 118 119 return implode("\n", $lines); 120 } 121 122 /** 123 * Generate a method-level docblock. 124 * 125 * @param array<array{name: string, type: string, description?: string}> $params 126 */ 127 public function generateMethodDocBlock( 128 ?string $description, 129 ?string $returnType, 130 array $params = [], 131 ?string $throws = null 132 ): string { 133 $lines = [' /**']; 134 135 // Add description 136 if ($description) { 137 $lines = array_merge($lines, $this->wrapDescription($description, ' * ')); 138 } 139 140 // Add blank line if we have params or return 141 if ((! empty($params) || $returnType) && $description) { 142 $lines[] = ' *'; 143 } 144 145 // Add parameters 146 foreach ($params as $param) { 147 $desc = $param['description'] ?? ''; 148 if ($desc) { 149 $lines[] = ' * @param '.$param['type'].' $'.$param['name'].' '.$desc; 150 } else { 151 $lines[] = ' * @param '.$param['type'].' $'.$param['name']; 152 } 153 } 154 155 // Add return type 156 if ($returnType && $returnType !== 'void') { 157 $lines[] = ' * @return '.$returnType; 158 } 159 160 // Add throws 161 if ($throws) { 162 $lines[] = ' * @throws '.$throws; 163 } 164 165 $lines[] = ' */'; 166 167 return implode("\n", $lines); 168 } 169 170 /** 171 * Wrap a long description into multiple lines. 172 * 173 * @return array<string> 174 */ 175 protected function wrapDescription(string $description, string $prefix = ' * '): array 176 { 177 $maxWidth = 80 - strlen($prefix); 178 $words = explode(' ', $description); 179 $lines = []; 180 $currentLine = ''; 181 182 foreach ($words as $word) { 183 if (empty($currentLine)) { 184 $currentLine = $word; 185 } elseif (strlen($currentLine.' '.$word) <= $maxWidth) { 186 $currentLine .= ' '.$word; 187 } else { 188 $lines[] = $prefix.$currentLine; 189 $currentLine = $word; 190 } 191 } 192 193 if (! empty($currentLine)) { 194 $lines[] = $prefix.$currentLine; 195 } 196 197 return $lines; 198 } 199 200 /** 201 * Extract validation constraints from properties. 202 * 203 * @param array<string, array<string, mixed>> $properties 204 * @param array<string> $required 205 * @return array<string> 206 */ 207 protected function extractConstraints(array $properties, array $required): array 208 { 209 $constraints = []; 210 211 // Required fields 212 if (! empty($required)) { 213 $constraints[] = 'Required: '.implode(', ', $required); 214 } 215 216 // Property-specific constraints 217 foreach ($properties as $name => $definition) { 218 $propConstraints = $this->extractPropertyConstraints($definition); 219 foreach ($propConstraints as $constraint) { 220 $constraints[] = $name.': '.trim(str_replace('@constraint', '', $constraint)); 221 } 222 } 223 224 return $constraints; 225 } 226 227 /** 228 * Extract validation constraints for a single property. 229 * 230 * @param array<string, mixed> $definition 231 * @return array<string> 232 */ 233 protected function extractPropertyConstraints(array $definition): array 234 { 235 $constraints = []; 236 237 // String constraints 238 if (isset($definition['maxLength'])) { 239 $constraints[] = '@constraint Max length: '.$definition['maxLength']; 240 } 241 242 if (isset($definition['minLength'])) { 243 $constraints[] = '@constraint Min length: '.$definition['minLength']; 244 } 245 246 if (isset($definition['maxGraphemes'])) { 247 $constraints[] = '@constraint Max graphemes: '.$definition['maxGraphemes']; 248 } 249 250 if (isset($definition['minGraphemes'])) { 251 $constraints[] = '@constraint Min graphemes: '.$definition['minGraphemes']; 252 } 253 254 // Number constraints 255 if (isset($definition['maximum'])) { 256 $constraints[] = '@constraint Maximum: '.$definition['maximum']; 257 } 258 259 if (isset($definition['minimum'])) { 260 $constraints[] = '@constraint Minimum: '.$definition['minimum']; 261 } 262 263 // Array constraints 264 if (isset($definition['maxItems'])) { 265 $constraints[] = '@constraint Max items: '.$definition['maxItems']; 266 } 267 268 if (isset($definition['minItems'])) { 269 $constraints[] = '@constraint Min items: '.$definition['minItems']; 270 } 271 272 // Enum constraints 273 if (isset($definition['enum'])) { 274 $values = is_array($definition['enum']) ? implode(', ', $definition['enum']) : $definition['enum']; 275 $constraints[] = '@constraint Enum: '.$values; 276 } 277 278 // Format constraints 279 if (isset($definition['format'])) { 280 $constraints[] = '@constraint Format: '.$definition['format']; 281 } 282 283 // Const constraint 284 if (isset($definition['const'])) { 285 $value = is_bool($definition['const']) ? ($definition['const'] ? 'true' : 'false') : $definition['const']; 286 $constraints[] = '@constraint Const: '.$value; 287 } 288 289 return $constraints; 290 } 291 292 /** 293 * Generate a simple docblock. 294 */ 295 public function generateSimple(string $description): string 296 { 297 return " /**\n * {$description}\n */"; 298 } 299 300 /** 301 * Generate a one-line docblock. 302 */ 303 public function generateOneLine(string $text): string 304 { 305 return " /** {$text} */"; 306 } 307}