Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at main 6.6 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Generator; 4 5class EnumGenerator 6{ 7 /** 8 * Naming converter for class/enum names. 9 */ 10 protected NamingConverter $naming; 11 12 /** 13 * File writer for writing generated files. 14 */ 15 protected FileWriter $fileWriter; 16 17 /** 18 * Base namespace for generated enums. 19 */ 20 protected string $baseNamespace; 21 22 /** 23 * Output directory for generated files. 24 */ 25 protected string $outputDirectory; 26 27 /** 28 * Create a new EnumGenerator. 29 */ 30 public function __construct( 31 string $baseNamespace = 'App\\Lexicons', 32 string $outputDirectory = 'app/Lexicons', 33 ?NamingConverter $naming = null, 34 ?FileWriter $fileWriter = null 35 ) { 36 $this->baseNamespace = rtrim($baseNamespace, '\\'); 37 $this->outputDirectory = rtrim($outputDirectory, '/'); 38 $this->naming = $naming ?? new NamingConverter($baseNamespace); 39 $this->fileWriter = $fileWriter ?? new FileWriter(); 40 } 41 42 /** 43 * Generate PHP enum from a string type with knownValues. 44 * 45 * @param string $nsid The NSID (e.g., "com.atproto.moderation.defs#reasonType") 46 * @param array $definition The lexicon definition 47 * @return string The generated enum code 48 */ 49 public function generate(string $nsid, array $definition): string 50 { 51 $type = $definition['type'] ?? null; 52 53 if ($type !== 'string' || ! isset($definition['knownValues'])) { 54 throw new \InvalidArgumentException("Definition must be a string type with knownValues"); 55 } 56 57 // Extract namespace and enum name from NSID 58 [$baseNsid, $defName] = $this->parseNsid($nsid); 59 60 $namespace = $this->naming->nsidToNamespace($baseNsid); 61 $enumName = $this->naming->toClassName($defName); 62 63 $description = $definition['description'] ?? ''; 64 $knownValues = $definition['knownValues']; 65 66 // Generate enum cases 67 $cases = $this->generateCases($knownValues); 68 69 return $this->renderEnum($namespace, $enumName, $description, $cases); 70 } 71 72 /** 73 * Parse NSID into base NSID and definition name. 74 * 75 * @return array{0: string, 1: string} 76 */ 77 protected function parseNsid(string $nsid): array 78 { 79 if (str_contains($nsid, '#')) { 80 [$baseNsid, $defName] = explode('#', $nsid, 2); 81 82 return [$baseNsid, $defName]; 83 } 84 85 // If no fragment, use the last part of the NSID as the enum name 86 $parts = explode('.', $nsid); 87 $defName = array_pop($parts); 88 $baseNsid = implode('.', $parts); 89 90 return [$baseNsid, $defName]; 91 } 92 93 /** 94 * Generate enum cases from known values. 95 * 96 * @param array<string> $knownValues 97 * @return array<array{name: string, value: string}> 98 */ 99 protected function generateCases(array $knownValues): array 100 { 101 $cases = []; 102 $usedNames = []; 103 104 foreach ($knownValues as $value) { 105 // Extract the case name from the value 106 // e.g., "com.atproto.moderation.defs#reasonSpam" -> "REASON_SPAM" 107 $caseName = $this->valueToCaseName($value); 108 109 // Handle duplicate case names by prepending the source namespace 110 if (isset($usedNames[$caseName])) { 111 // Get the source namespace (e.g., "tools.ozone.report" from "tools.ozone.report.defs#reasonAppeal") 112 if (str_contains($value, '#')) { 113 $nsid = explode('#', $value)[0]; 114 $parts = explode('.', $nsid); 115 // Use the second-to-last part as a differentiator (e.g., "Ozone", "Report") 116 $diff = ucfirst($parts[count($parts) - 2] ?? $parts[count($parts) - 1]); 117 $caseName = $diff . $caseName; 118 } 119 } 120 121 $usedNames[$caseName] = true; 122 $cases[] = [ 123 'name' => $caseName, 124 'value' => $value, 125 ]; 126 } 127 128 return $cases; 129 } 130 131 /** 132 * Convert a known value to an enum case name. 133 */ 134 protected function valueToCaseName(string $value): string 135 { 136 // If it's an NSID reference, extract the fragment part 137 if (str_contains($value, '#')) { 138 $value = explode('#', $value)[1]; 139 } 140 141 // Remove leading symbols (!, etc.) 142 $value = ltrim($value, '!@#$%^&*()-_=+[]{}|;:,.<>?/~`'); 143 144 // Convert kebab-case and snake_case to PascalCase 145 // e.g., "no-promote" -> "NoPromote", "dmca-violation" -> "DmcaViolation" 146 $value = str_replace(['-', '_'], ' ', $value); 147 $value = ucwords($value); 148 $value = str_replace(' ', '', $value); 149 150 // Ensure first character is uppercase 151 return ucfirst($value); 152 } 153 154 /** 155 * Render the enum code. 156 * 157 * @param array<array{name: string, value: string}> $cases 158 */ 159 protected function renderEnum(string $namespace, string $enumName, string $description, array $cases): string 160 { 161 $code = "<?php\n\n"; 162 $code .= "namespace {$namespace};\n\n"; 163 164 $code .= "/**\n"; 165 $code .= " * GENERATED CODE - DO NOT EDIT\n"; 166 167 if ($description) { 168 $code .= " *\n"; 169 $code .= " * " . str_replace("\n", "\n * ", $description) . "\n"; 170 } 171 172 $code .= " */\n"; 173 174 $code .= "enum {$enumName}: string\n"; 175 $code .= "{\n"; 176 177 foreach ($cases as $case) { 178 $code .= " case {$case['name']} = '{$case['value']}';\n"; 179 } 180 181 $code .= "}\n"; 182 183 return $code; 184 } 185 186 /** 187 * Generate and save enum to disk. 188 */ 189 public function generateAndSave(string $nsid, array $definition): string 190 { 191 $code = $this->generate($nsid, $definition); 192 193 [$baseNsid, $defName] = $this->parseNsid($nsid); 194 $namespace = $this->naming->nsidToNamespace($baseNsid); 195 $enumName = $this->naming->toClassName($defName); 196 197 $filePath = $this->getFilePath($namespace, $enumName); 198 $this->fileWriter->write($filePath, $code); 199 200 return $filePath; 201 } 202 203 /** 204 * Get the file path for a generated enum. 205 */ 206 protected function getFilePath(string $namespace, string $enumName): string 207 { 208 // Remove base namespace from full namespace 209 $relativePath = str_replace($this->baseNamespace.'\\', '', $namespace); 210 $relativePath = str_replace('\\', '/', $relativePath); 211 212 return $this->outputDirectory.'/'.$relativePath.'/'.$enumName.'.php'; 213 } 214}