diff --git a/docs/README.md b/docs/README.md index 7df18aa..d4cea70 100644 --- a/docs/README.md +++ b/docs/README.md @@ -721,6 +721,7 @@ bin/console scheduler:list bin/console scheduler:list --next=3 bin/console scheduler:list --timezone=Europe/Prague bin/console scheduler:list --explain +bin/console scheduler:list --explain=en ``` Options: @@ -730,8 +731,10 @@ Options: - `-v` - display absolute times - `--timezone` (or `-tz`) - display times in specified timezone instead of one used by application - e.g. `--tz=UTC` -- `--explain` - explain whole expression, including [seconds](#seconds) and [timezones](#timezones) +- `--explain[=]` - explain whole expression, including [seconds](#seconds) and [timezones](#timezones) - [Explain command](#explain-command) with `--id` parameter can be used to explain specific job + - e.g. `--explain` + - e.g. `--explain=en` (to choose language) ### Worker command @@ -752,8 +755,8 @@ Explain cron expression syntax bin/console scheduler:explain bin/console scheduler:explain --id="job id" bin/console scheduler:explain --expression="0 22 * 12 *" -bin/console scheduler:explain --expression="* 8 * * *" --seconds=10 --timezone="Europe/Prague" -bin/console scheduler:explain -e="* 8 * * *" -s=10 -tz="Europe/Prague" +bin/console scheduler:explain --expression="* 8 * * *" --seconds=10 --timezone="Europe/Prague" --language=en +bin/console scheduler:explain -e="* 8 * * *" -s=10 -tz="Europe/Prague" -l=en ``` Options: @@ -763,6 +766,7 @@ Options: - `--expression=` (or `-e`) - explain expression - `--seconds=` (or `-s`) - repeat every n seconds - `--timezone=` (or `-tz`) - the timezone time should be displayed in +- `--language=` (or `-l`) - explain in specified language ## Lazy loading diff --git a/src/Command/BaseExplainCommand.php b/src/Command/BaseExplainCommand.php index 5dc248a..0267edc 100644 --- a/src/Command/BaseExplainCommand.php +++ b/src/Command/BaseExplainCommand.php @@ -4,9 +4,12 @@ use DateTimeZone; use Orisai\Clock\SystemClock; +use Orisai\CronExpressionExplainer\CronExpressionExplainer; +use Orisai\CronExpressionExplainer\DefaultCronExpressionExplainer; use Orisai\Scheduler\Job\JobSchedule; use Psr\Clock\ClockInterface; use Symfony\Component\Console\Command\Command; +use function array_key_last; /** * @internal @@ -14,12 +17,30 @@ abstract class BaseExplainCommand extends Command { + protected CronExpressionExplainer $explainer; + protected ClockInterface $clock; - public function __construct(?ClockInterface $clock) + public function __construct(?CronExpressionExplainer $explainer, ?ClockInterface $clock) { - parent::__construct(); + $this->explainer = $explainer ?? new DefaultCronExpressionExplainer(); $this->clock = $clock ?? new SystemClock(); + parent::__construct(); + } + + protected function getSupportedLanguages(): string + { + $string = ''; + $languages = $this->explainer->getSupportedLanguages(); + $last = array_key_last($languages); + foreach ($languages as $code => $name) { + $string .= "$code ($name)"; + if ($code !== $last) { + $string .= ', '; + } + } + + return $string; } protected function computeTimeZone(JobSchedule $jobSchedule, DateTimeZone $renderedTimeZone): ?DateTimeZone diff --git a/src/Command/ExplainCommand.php b/src/Command/ExplainCommand.php index be38f6c..b898df0 100644 --- a/src/Command/ExplainCommand.php +++ b/src/Command/ExplainCommand.php @@ -4,13 +4,13 @@ use DateTimeZone; use Orisai\CronExpressionExplainer\CronExpressionExplainer; -use Orisai\CronExpressionExplainer\DefaultCronExpressionExplainer; -use Orisai\CronExpressionExplainer\Exception\InvalidExpression; +use Orisai\CronExpressionExplainer\Exception\UnsupportedExpression; use Orisai\Scheduler\Scheduler; use Psr\Clock\ClockInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function array_key_exists; use function assert; use function in_array; use function is_string; @@ -22,17 +22,14 @@ final class ExplainCommand extends BaseExplainCommand private Scheduler $scheduler; - private CronExpressionExplainer $explainer; - public function __construct( Scheduler $scheduler, ?CronExpressionExplainer $explainer = null, ?ClockInterface $clock = null ) { - parent::__construct($clock); + parent::__construct($explainer, $clock); $this->scheduler = $scheduler; - $this->explainer = $explainer ?? new DefaultCronExpressionExplainer(); } public static function getDefaultName(): string @@ -53,6 +50,12 @@ protected function configure(): void $this->addOption('expression', 'e', InputOption::VALUE_REQUIRED, 'Expression to explain'); $this->addOption('seconds', 's', InputOption::VALUE_REQUIRED, 'Repeat every n seconds'); $this->addOption('timezone', 'tz', InputOption::VALUE_REQUIRED, 'The timezone time should be displayed in'); + $this->addOption( + 'language', + 'l', + InputOption::VALUE_REQUIRED, + "Translate expression in given language - {$this->getSupportedLanguages()}", + ); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -66,20 +69,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int $expression = $options['expression']; $seconds = $options['seconds']; $timezone = $options['timezone']; + $language = $options['language']; if ($id !== null) { return $this->explainJobWithId($id, $output); } if ($expression !== null) { - return $this->explainExpression($expression, $seconds, $timezone, $output); + return $this->explainExpression($expression, $seconds, $timezone, $language, $output); } return $this->explainSyntax($output); } /** - * @return array{id: string|null, expression: string|null, seconds: int<0, 59>|null, timezone: DateTimeZone|null}|null + * @return array{id: string|null, expression: string|null, seconds: int<0, 59>|null, timezone: DateTimeZone|null, language: string|null}|null */ private function validateOptions(InputInterface $input, OutputInterface $output): ?array { @@ -141,6 +145,23 @@ private function validateOptions(InputInterface $input, OutputInterface $output) } } + $language = $input->getOption('language'); + assert($language === null || is_string($language)); + if ($language !== null) { + if (!array_key_exists($language, $this->explainer->getSupportedLanguages())) { + $hasErrors = true; + $output->writeln( + "Option --language expects no value or one of supported languages, '$language' given." + . ' Use --help to list available languages.', + ); + } + + if ($expression === null) { + $hasErrors = true; + $output->writeln('Option --language must be used with --expression.'); + } + } + if ($hasErrors) { return null; } @@ -154,6 +175,7 @@ private function validateOptions(InputInterface $input, OutputInterface $output) 'expression' => $expression, 'seconds' => $seconds, 'timezone' => $timezone, + 'language' => $language, ]; } @@ -164,6 +186,7 @@ private function explainExpression( string $expression, ?int $seconds, ?DateTimeZone $timeZone, + ?string $language, OutputInterface $output ): int { @@ -172,8 +195,9 @@ private function explainExpression( $expression, $seconds, $timeZone, + $language, )); - } catch (InvalidExpression $exception) { + } catch (UnsupportedExpression $exception) { $output->writeln("{$exception->getMessage()}"); return self::FAILURE; diff --git a/src/Command/ListCommand.php b/src/Command/ListCommand.php index c0a5c81..5098387 100644 --- a/src/Command/ListCommand.php +++ b/src/Command/ListCommand.php @@ -7,7 +7,6 @@ use DateTimeInterface; use DateTimeZone; use Orisai\CronExpressionExplainer\CronExpressionExplainer; -use Orisai\CronExpressionExplainer\DefaultCronExpressionExplainer; use Orisai\Scheduler\Job\JobSchedule; use Orisai\Scheduler\Scheduler; use Psr\Clock\ClockInterface; @@ -16,6 +15,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Terminal; use function abs; +use function array_key_exists; use function assert; use function floor; use function in_array; @@ -39,17 +39,14 @@ final class ListCommand extends BaseExplainCommand private Scheduler $scheduler; - private CronExpressionExplainer $explainer; - public function __construct( Scheduler $scheduler, ?ClockInterface $clock = null, ?CronExpressionExplainer $explainer = null ) { - parent::__construct($clock); + parent::__construct($explainer, $clock); $this->scheduler = $scheduler; - $this->explainer = $explainer ?? new DefaultCronExpressionExplainer(); } public static function getDefaultName(): string @@ -68,7 +65,12 @@ protected function configure(): void parent::configure(); $this->addOption('next', null, InputOption::VALUE_OPTIONAL, 'Sort jobs by their next execution time', false); $this->addOption('timezone', 'tz', InputOption::VALUE_REQUIRED, 'The timezone times should be displayed in'); - $this->addOption('explain', null, InputOption::VALUE_NONE, 'Explain expression'); + $this->addOption( + 'explain', + null, + InputOption::VALUE_OPTIONAL, + "Explain expression - {$this->getSupportedLanguages()}", + ); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -135,12 +137,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $nextDueDate, )); - if ($explain) { - $output->writeln($this->explainer->explain( + if ($explain !== null) { + $explainedExpression = $this->explainer->explain( $jobSchedule->getExpression()->getExpression(), $jobSchedule->getRepeatAfterSeconds(), $computedTimeZone, - )); + $explain === true ? null : $explain, + ); + + $output->writeln($explainedExpression); } } @@ -148,7 +153,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @return array{next: int<1, max>|bool, timezone: DateTimeZone, explain: bool}|null + * @return array{next: int<1, max>|bool, timezone: DateTimeZone, explain: true|string|null}|null */ private function validateOptions(InputInterface $input, OutputInterface $output): ?array { @@ -185,7 +190,17 @@ private function validateOptions(InputInterface $input, OutputInterface $output) } $explain = $input->getOption('explain'); - assert(is_bool($explain)); + assert($explain === null || $explain === true || is_string($explain)); + if ( + is_string($explain) + && !array_key_exists($explain, $this->explainer->getSupportedLanguages()) + ) { + $hasErrors = true; + $output->writeln( + "Option --explain expects no value or one of supported languages, '$explain' given." + . ' Use --help to list available languages.', + ); + } if ($hasErrors) { return null; diff --git a/tests/Unit/Command/ExplainCommandTest.php b/tests/Unit/Command/ExplainCommandTest.php index ca758ca..9df2ad2 100644 --- a/tests/Unit/Command/ExplainCommandTest.php +++ b/tests/Unit/Command/ExplainCommandTest.php @@ -229,6 +229,17 @@ public function provideExplainExpression(): Generator <<<'MSG' At every 59 seconds in UTC time zone. +MSG, + ]; + + yield [ + [ + '--expression' => '* * * * *', + '--language' => 'en', + ], + <<<'MSG' +At every minute. + MSG, ]; @@ -237,6 +248,7 @@ public function provideExplainExpression(): Generator '-e' => '* * * * *', '-s' => '59', '-tz' => 'UTC', + '-l' => 'en', ], <<<'MSG' At every 59 seconds in UTC time zone. @@ -348,6 +360,27 @@ public function provideInputError(): Generator <<<'MSG' Option --timezone must be used with --expression. +MSG, + ]; + + yield [ + [ + '--expression' => '* * * *', + '--language' => 'noop', + ], + <<<'MSG' +Option --language expects no value or one of supported languages, 'noop' given. Use --help to list available languages. + +MSG, + ]; + + yield [ + [ + '--language' => 'en', + ], + <<<'MSG' +Option --language must be used with --expression. + MSG, ]; @@ -356,6 +389,7 @@ public function provideInputError(): Generator '--id' => 'id', '--seconds' => 'bad seconds', '--timezone' => 'bad timezone', + '--language' => 'noop', ], <<<'MSG' Option --seconds expects an int<0, 59>, 'bad seconds' given. @@ -364,6 +398,8 @@ public function provideInputError(): Generator Option --timezone expects a valid timezone, 'bad timezone' given. Option --timezone cannot be used with --id. Option --timezone must be used with --expression. +Option --language expects no value or one of supported languages, 'noop' given. Use --help to list available languages. +Option --language must be used with --expression. MSG, ]; diff --git a/tests/Unit/Command/ListCommandTest.php b/tests/Unit/Command/ListCommandTest.php index 2287f96..9800ff9 100644 --- a/tests/Unit/Command/ListCommandTest.php +++ b/tests/Unit/Command/ListCommandTest.php @@ -310,6 +310,16 @@ public function provideInputError(): Generator <<<'MSG' Option --timezone expects a valid timezone, 'bad-timezone' given. +MSG, + ]; + + yield [ + [ + '--explain' => 'noop', + ], + <<<'MSG' +Option --explain expects no value or one of supported languages, 'noop' given. Use --help to list available languages. + MSG, ]; @@ -317,10 +327,12 @@ public function provideInputError(): Generator [ '--next' => '0', '--timezone' => 'bad-timezone', + '--explain' => 'noop', ], <<<'MSG' Option --next expects an int<1, max>, '0' given. Option --timezone expects a valid timezone, 'bad-timezone' given. +Option --explain expects no value or one of supported languages, 'noop' given. Use --help to list available languages. MSG, ]; @@ -424,7 +436,7 @@ public function testExplain(): void ]); self::assertSame( - <<<'MSG' + $explainDefault = <<<'MSG' * * * 4 * / 10 [2] Tests\Orisai\Scheduler\Doubles\CallbackList::__invoke() Next Due: 2 months At every 10 seconds in April. * * * * * [0] Tests\Orisai\Scheduler\Doubles\CallbackList::job1() Next Due: 59 seconds @@ -436,6 +448,16 @@ public function testExplain(): void CommandOutputHelper::getCommandOutput($tester), ); self::assertSame($command::SUCCESS, $tester->getStatusCode()); + + $tester->execute([ + '--explain' => 'en', + ]); + + self::assertSame( + $explainDefault, + CommandOutputHelper::getCommandOutput($tester), + ); + self::assertSame($command::SUCCESS, $tester->getStatusCode()); } }