Lines
100.00%
68 / 68
Functions and Methods
100.00%
13 / 13
Classes and Traits
100.00%
1 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| GenerateCommand | 100.00% | 68 / 68 | 100.00% | 13 / 13 | 28 | 100.00% | 1 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| configure | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 1 | |||||
| execute | 100.00% | 10 / 10 | 100.00% | 1 / 1 | 3 | |||||
| copySnapshotIfRequested | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | |||||
| generateFromSnapshot | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 2 | |||||
| selectShrinkPolicy | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 2 | |||||
| loadSnapshot | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 2 | |||||
| describeAutoloadMismatch | 100.00% | 12 / 12 | 100.00% | 1 / 1 | 3 | |||||
| printGenerationSummary | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 1 | |||||
| printCheckResult | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 3 | |||||
| printWarnings | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | |||||
| printFailure | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | |||||
| normalizePath | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 5 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Stewart\Codegen\Console; | |
| 6 | ||
| 7 | use Stewart\Codegen\GenerationTarget; | |
| 8 | use Stewart\Codegen\Generator; | |
| 9 | use Stewart\Codegen\Model\GenerationWarning; | |
| 10 | use Stewart\Codegen\Output\CheckResult; | |
| 11 | use Stewart\Codegen\Output\FileChange; | |
| 12 | use Stewart\Codegen\Output\GenerationReport; | |
| 13 | use Stewart\Codegen\Output\ShrinkPolicy; | |
| 14 | use Stewart\Codegen\Snapshot\Snapshot; | |
| 15 | use Stewart\Codegen\Snapshot\SnapshotFetcher; | |
| 16 | use Stewart\Codegen\Snapshot\SnapshotFileReader; | |
| 17 | use Stewart\Codegen\Snapshot\SnapshotFileWriter; | |
| 18 | use Stewart\Contracts\Exception\StewartException; | |
| 19 | use Stewart\Runtime\Config\CodegenConfig; | |
| 20 | use Stewart\Runtime\Config\ConfigLoader; | |
| 21 | use Stewart\Runtime\Config\ProjectRoot; | |
| 22 | use Stewart\Runtime\Config\Psr4Map; | |
| 23 | use Stewart\Runtime\Config\StewartConfig; | |
| 24 | use Stewart\Runtime\Console\StewartCommand; | |
| 25 | use Stewart\Runtime\Logging\LoggerFactory; | |
| 26 | use Symfony\Component\Console\Attribute\AsCommand; | |
| 27 | use Symfony\Component\Console\Command\Command; | |
| 28 | use Symfony\Component\Console\Formatter\OutputFormatter; | |
| 29 | use Symfony\Component\Console\Helper\Table; | |
| 30 | use Symfony\Component\Console\Input\InputInterface; | |
| 31 | use Symfony\Component\Console\Input\InputOption; | |
| 32 | use Symfony\Component\Console\Output\OutputInterface; | |
| 33 | ||
| 34 | #[AsCommand(name: self::NAME, description: 'Write typed entity and service classes from your Home Assistant')] | |
| 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 | /** @throws StewartException */ | |
| 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 | /** @throws StewartException */ | |
| 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 | // Compared as strings: realpath fails while the directory does not exist yet. | |
| 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 | /** @param list<GenerationWarning> $warnings */ | |
| 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 | // Composer's base directory contains `..`, and realpath fails before the first run. | |
| 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 | } |