| 35 | | final class GenerateCommand extends StewartCommand |
| 36 | | { |
| 37 | | public const string NAME = 'generate'; |
| 38 | | |
| 39 | | public function __construct( |
| 40 | | private readonly ConfigLoader $config, |
| 41 | | private readonly Psr4Map $psr4Map, |
| 42 | | private readonly ProjectRoot $projectRoot, |
| 43 | | private readonly LoggerFactory $loggers, |
| 44 | | private readonly SnapshotFileReader $snapshotReader, |
| 45 | | private readonly SnapshotFileWriter $snapshotWriter, |
| 46 | | private readonly SnapshotFetcher $snapshotFetcher, |
| 47 | | private readonly Generator $generator, |
| 48 | | private readonly GenerationOptionsFactory $generationOptionsFactory, |
| 49 | | ) { |
| 50 | | parent::__construct(); |
| 51 | | } |
| 52 | | |
| 53 | | protected function configure(): void |
| 54 | | { |
| 55 | | parent::configure(); |
| 56 | | $this |
| 57 | | ->addOption('snapshot-in', null, InputOption::VALUE_REQUIRED, 'Generate from this file instead of connecting') |
| 58 | | ->addOption('snapshot-out', null, InputOption::VALUE_REQUIRED, 'Write what was read to this file') |
| 59 | | ->addOption('check', null, InputOption::VALUE_NONE, 'Report what would change and fail if anything would') |
| 60 | | ->addOption('allow-shrink', null, InputOption::VALUE_NONE, 'Write even if that deletes more than half of the generated files'); |
| 61 | | } |
| 62 | | |
| 63 | | protected function execute(InputInterface $input, OutputInterface $output): int |
| 64 | | { |
| 65 | | try { |
| 66 | | $config = $this->config->loadConfig($this->findConfigOption($input)); |
| 67 | | $mismatch = $this->describeAutoloadMismatch($config->codegen); |
| 68 | | |
| 69 | | if ($mismatch !== null) { |
| 70 | | $output->writeln($mismatch); |
| 71 | | |
| 72 | | return Command::FAILURE; |
| 73 | | } |
| 74 | | |
| 75 | | $snapshot = $this->loadSnapshot($input, $config); |
| 76 | | $this->copySnapshotIfRequested($input, $snapshot); |
| 77 | | |
| 78 | | return $this->generateFromSnapshot($input, $output, $config->codegen, $snapshot); |
| 79 | | } catch (StewartException $e) { |
| 80 | | return $this->printFailure($output, $e->getMessage()); |
| 81 | | } |
| 82 | | } |
| 83 | | |
| 84 | | |
| 85 | | private function copySnapshotIfRequested(InputInterface $input, Snapshot $snapshot): void |
| 86 | | { |
| 87 | | $snapshotOut = $this->nonEmptyStringOption($input, 'snapshot-out'); |
| 88 | | |
| 89 | | if ($snapshotOut !== null) { |
| 90 | | $this->snapshotWriter->writeSnapshot($snapshotOut, $snapshot); |
| 91 | | } |
| 92 | | } |
| 93 | | |
| 94 | | |
| 95 | | private function generateFromSnapshot(InputInterface $input, OutputInterface $output, CodegenConfig $codegen, Snapshot $snapshot): int |
| 96 | | { |
| 97 | | $target = new GenerationTarget($codegen->namespace->value, $codegen->outputDir->resolveWithin($this->projectRoot->path)); |
| 98 | | $options = $this->generationOptionsFactory->createFromCodegenConfig($codegen); |
| 99 | | |
| 100 | | return $input->getOption('check') === true |
| 101 | | ? $this->printCheckResult($this->generator->checkAgainstSnapshot($snapshot, $target, $options), $output) |
| 102 | | : $this->printGenerationSummary($this->generator->writeFiles($snapshot, $target, $options, $this->selectShrinkPolicy($input)), $output); |
| 103 | | } |
| 104 | | |
| 105 | | private function selectShrinkPolicy(InputInterface $input): ShrinkPolicy |
| 106 | | { |
| 107 | | return $input->getOption('allow-shrink') === true ? ShrinkPolicy::Allow : ShrinkPolicy::Refuse; |
| 108 | | } |
| 109 | | |
| 110 | | private function loadSnapshot(InputInterface $input, StewartConfig $config): Snapshot |
| 111 | | { |
| 112 | | $snapshotIn = $this->nonEmptyStringOption($input, 'snapshot-in'); |
| 113 | | $logger = $this->loggers->createStdoutLogger($config->logLevel, $config->logFormat); |
| 114 | | |
| 115 | | if ($snapshotIn !== null) { |
| 116 | | return $this->snapshotReader->readSnapshot($snapshotIn, $logger); |
| 117 | | } |
| 118 | | |
| 119 | | return $this->snapshotFetcher->fetchSnapshot($config->requireHomeAssistant(), $logger); |
| 120 | | } |
| 121 | | |
| 122 | | |
| 123 | | private function describeAutoloadMismatch(CodegenConfig $codegen): ?string |
| 124 | | { |
| 125 | | $expected = $this->normalizePath($codegen->outputDir->resolveWithin($this->projectRoot->path)); |
| 126 | | |
| 127 | | foreach ($this->psr4Map->listDirectoriesFor($codegen->namespace->value . '\\') as $directory) { |
| 128 | | if ($this->normalizePath($directory) === $expected) { |
| 129 | | return null; |
| 130 | | } |
| 131 | | } |
| 132 | | |
| 133 | | return \sprintf( |
| 134 | | "<error>Composer does not map %s\\ to %s/.</error>\nAdd this to composer.json, then run `make install`:\n\n" |
| 135 | | . " \"autoload\": {\n \"psr-4\": {\n \"%s\\\\\": \"%s/\"\n }\n }\n", |
| 136 | | $codegen->namespace->value, |
| 137 | | $codegen->outputDir->value, |
| 138 | | str_replace('\\', '\\\\', $codegen->namespace->value), |
| 139 | | $codegen->outputDir->value, |
| 140 | | ); |
| 141 | | } |
| 142 | | |
| 143 | | private function printGenerationSummary(GenerationReport $report, OutputInterface $output): int |
| 144 | | { |
| 145 | | $this->printWarnings($output, $report->warnings); |
| 146 | | $output->writeln($report->summary()); |
| 147 | | |
| 148 | | return Command::SUCCESS; |
| 149 | | } |
| 150 | | |
| 151 | | private function printCheckResult(CheckResult $result, OutputInterface $output): int |
| 152 | | { |
| 153 | | $this->printWarnings($output, $result->warnings); |
| 154 | | |
| 155 | | if ($result->isClean()) { |
| 156 | | $output->writeln('<info>The generated classes match Home Assistant.</info>'); |
| 157 | | |
| 158 | | return Command::SUCCESS; |
| 159 | | } |
| 160 | | |
| 161 | | $rows = $result->listStaleFiles()->mapToList( |
| 162 | | static fn(FileChange $change): array => [$change->path, $change->outcome->value], |
| 163 | | ); |
| 164 | | |
| 165 | | if ($rows !== []) { |
| 166 | | new Table($output)->setStyle('compact')->setHeaders(['file', 'would be'])->setRows($rows)->render(); |
| 167 | | } |
| 168 | | |
| 169 | | $output->writeln('<comment>Run `make generate`.</comment>'); |
| 170 | | |
| 171 | | return Command::FAILURE; |
| 172 | | } |
| 173 | | |
| 174 | | |
| 175 | | private function printWarnings(OutputInterface $output, array $warnings): void |
| 176 | | { |
| 177 | | foreach ($warnings as $warning) { |
| 178 | | $output->writeln('<comment>' . OutputFormatter::escape($warning->describeWarning()) . '</comment>'); |
| 179 | | } |
| 180 | | } |
| 181 | | |
| 182 | | private function printFailure(OutputInterface $output, string $message): int |
| 183 | | { |
| 184 | | $output->writeln('<error>' . $message . '</error>'); |
| 185 | | |
| 186 | | return Command::FAILURE; |
| 187 | | } |
| 188 | | |
| 189 | | |
| 190 | | private function normalizePath(string $path): string |
| 191 | | { |
| 192 | | $segments = []; |
| 193 | | |
| 194 | | foreach (explode('/', str_replace('\\', '/', $path)) as $segment) { |
| 195 | | match ($segment) { |
| 196 | | '', '.' => null, |
| 197 | | '..' => array_pop($segments), |
| 198 | | default => $segments[] = $segment, |
| 199 | | }; |
| 200 | | } |
| 201 | | |
| 202 | | return '/' . implode('/', $segments); |
| 203 | | } |
| 204 | | } |