Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 8.4 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Console; 4 5use Illuminate\Console\Command; 6use SocialDept\AtpSchema\Generator\DTOGenerator; 7use SocialDept\AtpSchema\Parser\SchemaLoader; 8 9class GenerateCommand extends Command 10{ 11 /** 12 * The name and signature of the console command. 13 */ 14 protected $signature = 'schema:generate 15 {nsid : The NSID of the schema to generate} 16 {--output= : Output directory for generated files} 17 {--namespace= : Base namespace for generated classes} 18 {--force : Overwrite existing files} 19 {--dry-run : Preview generated code without writing files} 20 {--with-dependencies : Also generate all referenced lexicons recursively} 21 {--r|recursive : Alias for --with-dependencies}'; 22 23 /** 24 * The console command description. 25 */ 26 protected $description = 'Generate PHP DTO classes from ATProto Lexicon schemas'; 27 28 /** 29 * Track generated NSIDs to avoid duplicates. 30 */ 31 protected array $generated = []; 32 33 /** 34 * Execute the console command. 35 */ 36 public function handle(): int 37 { 38 $nsid = $this->argument('nsid'); 39 $output = $this->option('output') ?? config('schema.lexicons.output_path'); 40 $namespace = $this->option('namespace') ?? config('schema.lexicons.base_namespace'); 41 $force = $this->option('force'); 42 $dryRun = $this->option('dry-run'); 43 $withDependencies = $this->option('with-dependencies') || $this->option('recursive'); 44 45 $this->info("Generating DTO classes for schema: {$nsid}"); 46 47 try { 48 $sources = config('schema.sources', []); 49 $loader = new SchemaLoader( 50 sources: $sources, 51 useCache: config('schema.cache.enabled', true), 52 cacheTtl: config('schema.cache.schema_ttl', 3600), 53 cachePrefix: config('schema.cache.prefix', 'schema'), 54 dnsResolutionEnabled: config('schema.dns_resolution.enabled', true), 55 httpTimeout: config('schema.http.timeout', 10) 56 ); 57 58 $generator = new DTOGenerator( 59 schemaLoader: $loader, 60 baseNamespace: $namespace, 61 outputDirectory: $output 62 ); 63 64 if ($dryRun) { 65 $this->info('Dry run mode - no files will be written'); 66 $document = $loader->load($nsid); 67 $code = $generator->preview($document); 68 69 $this->line(''); 70 $this->line('Generated code:'); 71 $this->line('─────────────────────────────────────────────────'); 72 $this->line($code); 73 $this->line('─────────────────────────────────────────────────'); 74 75 return self::SUCCESS; 76 } 77 78 $allFiles = []; 79 80 if ($withDependencies) { 81 $this->info('Generating with dependencies...'); 82 $allFiles = $this->generateWithDependencies($nsid, $loader, $generator, $force); 83 } else { 84 $allFiles = $generator->generateByNsid($nsid, [ 85 'dryRun' => false, 86 'overwrite' => $force, 87 ]); 88 } 89 90 $this->newLine(); 91 $this->info('Generated '.count($allFiles).' file(s):'); 92 93 foreach ($allFiles as $file) { 94 $this->line(" - {$file}"); 95 } 96 97 $this->newLine(); 98 $this->info('✓ Generation completed successfully'); 99 100 return self::SUCCESS; 101 } catch (\Exception $e) { 102 $this->error('Generation failed: '.$e->getMessage()); 103 104 if ($this->output->isVerbose()) { 105 $this->error($e->getTraceAsString()); 106 } 107 108 return self::FAILURE; 109 } 110 } 111 112 /** 113 * Generate schema with all its dependencies recursively. 114 */ 115 protected function generateWithDependencies( 116 string $nsid, 117 SchemaLoader $loader, 118 DTOGenerator $generator, 119 bool $force 120 ): array { 121 // Skip if already generated 122 if (in_array($nsid, $this->generated)) { 123 return []; 124 } 125 126 $this->line(" → Loading schema: {$nsid}"); 127 128 try { 129 $schema = $loader->load($nsid); 130 } catch (\Exception $e) { 131 $this->warn(" ⚠ Could not load {$nsid}: ".$e->getMessage()); 132 133 return []; 134 } 135 136 // Extract all referenced NSIDs from this schema 137 $dependencies = $this->extractDependencies($schema); 138 139 $allFiles = []; 140 141 // Generate dependencies first 142 foreach ($dependencies as $depNsid) { 143 $depFiles = $this->generateWithDependencies($depNsid, $loader, $generator, $force); 144 $allFiles = array_merge($allFiles, $depFiles); 145 } 146 147 // Mark as generated before generating to prevent circular references 148 $this->generated[] = $nsid; 149 150 // Generate current schema 151 try { 152 $files = $generator->generateByNsid($nsid, [ 153 'dryRun' => false, 154 'overwrite' => $force, 155 ]); 156 $allFiles = array_merge($allFiles, $files); 157 } catch (\Exception $e) { 158 $this->warn(" ⚠ Could not generate {$nsid}: ".$e->getMessage()); 159 } 160 161 return $allFiles; 162 } 163 164 /** 165 * Extract all NSID dependencies from a schema. 166 */ 167 protected function extractDependencies(\SocialDept\AtpSchema\Data\LexiconDocument $schema): array 168 { 169 $dependencies = []; 170 $currentNsid = $schema->getNsid(); 171 172 // Walk through all definitions 173 foreach ($schema->defs as $def) { 174 $dependencies = array_merge($dependencies, $this->extractRefsFromDefinition($def)); 175 } 176 177 // Filter out refs that are definitions within the same schema 178 // (refs that start with the current NSID followed by a dot) 179 $dependencies = array_filter($dependencies, function ($ref) use ($currentNsid) { 180 return ! str_starts_with($ref, $currentNsid.'.'); 181 }); 182 183 return array_unique($dependencies); 184 } 185 186 /** 187 * Recursively extract refs from a definition. 188 */ 189 protected function extractRefsFromDefinition(array $definition): array 190 { 191 $refs = []; 192 193 // Handle direct ref 194 if (isset($definition['ref'])) { 195 $ref = $definition['ref']; 196 // Skip local references (starting with #) 197 if (! str_starts_with($ref, '#')) { 198 // Extract NSID part (before fragment) 199 if (str_contains($ref, '#')) { 200 $ref = explode('#', $ref)[0]; 201 } 202 $refs[] = $ref; 203 } 204 } 205 206 // Handle union refs 207 if (isset($definition['refs']) && is_array($definition['refs'])) { 208 foreach ($definition['refs'] as $ref) { 209 // Skip local references 210 if (! str_starts_with($ref, '#')) { 211 // Extract NSID part 212 if (str_contains($ref, '#')) { 213 $ref = explode('#', $ref)[0]; 214 } 215 $refs[] = $ref; 216 } 217 } 218 } 219 220 // Recursively check properties 221 if (isset($definition['properties']) && is_array($definition['properties'])) { 222 foreach ($definition['properties'] as $propDef) { 223 $refs = array_merge($refs, $this->extractRefsFromDefinition($propDef)); 224 } 225 } 226 227 // Recursively check record 228 if (isset($definition['record']) && is_array($definition['record'])) { 229 $refs = array_merge($refs, $this->extractRefsFromDefinition($definition['record'])); 230 } 231 232 // Recursively check array items 233 if (isset($definition['items']) && is_array($definition['items'])) { 234 $refs = array_merge($refs, $this->extractRefsFromDefinition($definition['items'])); 235 } 236 237 return $refs; 238 } 239}