baseNamespace = rtrim($baseNamespace, '\\'); $this->outputDirectory = rtrim($outputDirectory, '/'); $this->naming = $naming ?? new NamingConverter($baseNamespace); $this->fileWriter = $fileWriter ?? new FileWriter(); } /** * Generate PHP enum from a string type with knownValues. * * @param string $nsid The NSID (e.g., "com.atproto.moderation.defs#reasonType") * @param array $definition The lexicon definition * @return string The generated enum code */ public function generate(string $nsid, array $definition): string { $type = $definition['type'] ?? null; if ($type !== 'string' || ! isset($definition['knownValues'])) { throw new \InvalidArgumentException("Definition must be a string type with knownValues"); } // Extract namespace and enum name from NSID [$baseNsid, $defName] = $this->parseNsid($nsid); $namespace = $this->naming->nsidToNamespace($baseNsid); $enumName = $this->naming->toClassName($defName); $description = $definition['description'] ?? ''; $knownValues = $definition['knownValues']; // Generate enum cases $cases = $this->generateCases($knownValues); return $this->renderEnum($namespace, $enumName, $description, $cases); } /** * Parse NSID into base NSID and definition name. * * @return array{0: string, 1: string} */ protected function parseNsid(string $nsid): array { if (str_contains($nsid, '#')) { [$baseNsid, $defName] = explode('#', $nsid, 2); return [$baseNsid, $defName]; } // If no fragment, use the last part of the NSID as the enum name $parts = explode('.', $nsid); $defName = array_pop($parts); $baseNsid = implode('.', $parts); return [$baseNsid, $defName]; } /** * Generate enum cases from known values. * * @param array $knownValues * @return array */ protected function generateCases(array $knownValues): array { $cases = []; $usedNames = []; foreach ($knownValues as $value) { // Extract the case name from the value // e.g., "com.atproto.moderation.defs#reasonSpam" -> "REASON_SPAM" $caseName = $this->valueToCaseName($value); // Handle duplicate case names by prepending the source namespace if (isset($usedNames[$caseName])) { // Get the source namespace (e.g., "tools.ozone.report" from "tools.ozone.report.defs#reasonAppeal") if (str_contains($value, '#')) { $nsid = explode('#', $value)[0]; $parts = explode('.', $nsid); // Use the second-to-last part as a differentiator (e.g., "Ozone", "Report") $diff = ucfirst($parts[count($parts) - 2] ?? $parts[count($parts) - 1]); $caseName = $diff . $caseName; } } $usedNames[$caseName] = true; $cases[] = [ 'name' => $caseName, 'value' => $value, ]; } return $cases; } /** * Convert a known value to an enum case name. */ protected function valueToCaseName(string $value): string { // If it's an NSID reference, extract the fragment part if (str_contains($value, '#')) { $value = explode('#', $value)[1]; } // Remove leading symbols (!, etc.) $value = ltrim($value, '!@#$%^&*()-_=+[]{}|;:,.<>?/~`'); // Convert kebab-case and snake_case to PascalCase // e.g., "no-promote" -> "NoPromote", "dmca-violation" -> "DmcaViolation" $value = str_replace(['-', '_'], ' ', $value); $value = ucwords($value); $value = str_replace(' ', '', $value); // Ensure first character is uppercase return ucfirst($value); } /** * Render the enum code. * * @param array $cases */ protected function renderEnum(string $namespace, string $enumName, string $description, array $cases): string { $code = "generate($nsid, $definition); [$baseNsid, $defName] = $this->parseNsid($nsid); $namespace = $this->naming->nsidToNamespace($baseNsid); $enumName = $this->naming->toClassName($defName); $filePath = $this->getFilePath($namespace, $enumName); $this->fileWriter->write($filePath, $code); return $filePath; } /** * Get the file path for a generated enum. */ protected function getFilePath(string $namespace, string $enumName): string { // Remove base namespace from full namespace $relativePath = str_replace($this->baseNamespace.'\\', '', $namespace); $relativePath = str_replace('\\', '/', $relativePath); return $this->outputDirectory.'/'.$relativePath.'/'.$enumName.'.php'; } }