Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add EnumGenerator for string types with knownValues

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