Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for getting raw parameters without validation #322

Merged
merged 1 commit into from Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -2,9 +2,11 @@

## Not released yet

* Add a option `ignoreValidationErrors` on `AsTask` attribute to ignore
parameters & options validation errors
* Add a way to merge an application `box.json` config file used by `castor:repack`command
* Deprecate `Context::withPath()` in favor of `Context::withCurrentDirectory()`
* Deprecate `path` argument in `capture()`, `exit_code()`, `run()`, `with()` in favor of `currentDirectory`
* Add a way to merge an application `box.json` config file used by `castor:repack`command

## 0.14.0 (2024-03-08)

Expand Down
3 changes: 2 additions & 1 deletion bin/castor
Expand Up @@ -2,6 +2,7 @@
<?php

use Castor\Console\ApplicationFactory;
use Castor\Console\Input\Input;

if (file_exists($file = __DIR__ . '/../vendor/autoload.php')) {
require $file;
Expand All @@ -11,4 +12,4 @@ if (file_exists($file = __DIR__ . '/../vendor/autoload.php')) {
throw new \RuntimeException('Unable to find autoloader.');
}

ApplicationFactory::create()->run();
ApplicationFactory::create()->run(new Input());
16 changes: 9 additions & 7 deletions bin/generate-tests.php
Expand Up @@ -116,19 +116,21 @@
add_test([], $class, '{{ base }}/tests/Examples/fixtures/broken/' . $dir->getRelativePath());
}

add_test(['parallel:sleep', '--sleep5', '0', '--sleep7', '0', '--sleep10', '0'], 'ParallelSleepTest');
add_test(['context:context', '--context', 'run'], 'ContextContextRunTest');
add_test(['args:passthru', 'a', 'b', '--no', '--foo', 'bar', '-x'], 'ArgPassthruExpanded');
add_test(['context:context', '--context', 'dynamic'], 'ContextContextDynamicTest');
add_test(['context:context', '--context', 'my_default', '-v'], 'ContextContextMyDefaultTest');
add_test(['context:context', '--context', 'no_no_exist'], 'ContextContextDoNotExistTest');
add_test(['context:context', '--context', 'production'], 'ContextContextProductionTest');
add_test(['context:context', '--context', 'path'], 'ContextContextPathTest');
add_test(['context:context', '--context', 'dynamic'], 'ContextContextDynamicTest');
add_test(['context:context', '--context', 'production'], 'ContextContextProductionTest');
add_test(['context:context', '--context', 'run'], 'ContextContextRunTest');
add_test(['enabled:hello', '--context', 'production'], 'EnabledInProduction');
add_test([], 'NewProjectTest', '/tmp');
add_test(['parallel:sleep', '--sleep5', '0', '--sleep7', '0', '--sleep10', '0'], 'ParallelSleepTest');
// In /tmp
add_test(['completion', 'bash'], 'NoConfigCompletionTest', '/tmp');
add_test(['init'], 'NewProjectInitTest', '/tmp');
add_test(['unknown:task'], 'NoConfigUnknownTest', '/tmp');
add_test(['unknown:task', 'toto', '--foo', 1], 'NoConfigUnknownWithArgsTest', '/tmp');
add_test(['completion', 'bash'], 'NoConfigCompletionTest', '/tmp');
add_test(['unknown:task'], 'NoConfigUnknownTest', '/tmp');
add_test([], 'NewProjectTest', '/tmp');

function add_test(array $args, string $class, ?string $cwd = null)
{
Expand Down
19 changes: 19 additions & 0 deletions doc/getting-started/arguments.md
Expand Up @@ -49,6 +49,25 @@ $ castor task --default=bar foo
foo bar
```

## Arguments without configuration nor validation

Castor supports the use of arguments without any configuration nor validation.
For example, when you want to call a sub-process:

```php
#[AsTask(ignoreValidationErrors: true)]
function phpunit(#[AsRawTokens] array $rawTokens): void
{
run(['phpunit', ...$rawTokens]);
}
```

Then, you can use it like that:

```bash
$ castor phpunit --filter=testName --debug --verbose
```

> [!TIP]
> Related example: [args.php](https://github.com/jolicode/castor/blob/main/examples/args.php)

Expand Down
1 change: 1 addition & 0 deletions doc/reference.md
Expand Up @@ -57,5 +57,6 @@ Castor provides the following attributes to register tasks, listener, etc:
- [`AsContextGenerator`](going-further/interacting-with-castor/advanced-context.md#the-ascontextgenerator-attribute)
- [`AsListener`](going-further/extending-castor/events.md#registering-a-listener)
- [`AsOption`](getting-started/arguments.md#overriding-the-option-name-and-description)
- [`AsRawTokens`](getting-started/arguments.md#arguments-without-configuration-nor-validation)
- [`AsSymfonyTask`](going-further/interacting-with-castor/symfony-task.md)
- [`AsTask`](getting-started/basic-usage.md)
10 changes: 10 additions & 0 deletions examples/args.php
Expand Up @@ -4,6 +4,7 @@

use Castor\Attribute\AsArgument;
use Castor\Attribute\AsOption;
use Castor\Attribute\AsRawTokens;
use Castor\Attribute\AsTask;

use function Castor\run;
Expand Down Expand Up @@ -32,3 +33,12 @@ function another_args(
): void {
run(['echo', $required, $test2]);
}

/**
* @param string[] $rawTokens
*/
#[AsTask(description: 'Dumps all arguments and options, without configuration nor validation', ignoreValidationErrors: true)]
function passthru(#[AsRawTokens] array $rawTokens): void
{
var_dump($rawTokens);
}
8 changes: 8 additions & 0 deletions src/Attribute/AsRawTokens.php
@@ -0,0 +1,8 @@
<?php

namespace Castor\Attribute;

#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AsRawTokens
{
}
1 change: 1 addition & 0 deletions src/Attribute/AsTask.php
Expand Up @@ -16,6 +16,7 @@ public function __construct(
public array $aliases = [],
public array $onSignals = [],
public string|bool $enabled = true,
public bool $ignoreValidationErrors = false,
) {
}
}
9 changes: 6 additions & 3 deletions src/Console/Application.php
Expand Up @@ -4,6 +4,7 @@

use Castor\Console\Command\SymfonyTaskCommand;
use Castor\Console\Command\TaskCommand;
use Castor\Console\Input\Input;
use Castor\Context;
use Castor\ContextDescriptor;
use Castor\ContextGeneratorDescriptor;
Expand Down Expand Up @@ -46,7 +47,7 @@ class Application extends SymfonyApplication
public const VERSION = 'v0.14.0';

// "Current" objects availables at some point of the lifecycle
private InputInterface $input;
private Input $input;
private SectionOutput $sectionOutput;
private SymfonyStyle $symfonyStyle;
private Command $command;
Expand Down Expand Up @@ -94,7 +95,7 @@ public function __construct(
GlobalHelper::setApplication($this);
}

public function getInput(): InputInterface
public function getInput(): Input
{
return $this->input ?? throw new \LogicException('Input not available yet.');
}
Expand Down Expand Up @@ -126,7 +127,9 @@ public function getCommand(bool $allowNull = false): ?Command
// is registered
public function doRun(InputInterface $input, OutputInterface $output): int
{
$this->input = $input;
if ($input instanceof Input) {
$this->input = $input;
}
$this->sectionOutput = new SectionOutput($output);
$this->symfonyStyle = new SymfonyStyle($input, $output);
$this->logger->pushHandler(new ConsoleHandler($output));
Expand Down
7 changes: 5 additions & 2 deletions src/Console/Command/SymfonyTaskCommand.php
Expand Up @@ -3,6 +3,7 @@
namespace Castor\Console\Command;

use Castor\Attribute\AsSymfonyTask;
use Castor\Console\Input\Input;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -67,10 +68,12 @@ protected function configure(): void
}
}

/**
* @param Input $input
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$r = new \ReflectionProperty($input, 'tokens');
$extra = array_filter($r->getValue($input), fn ($item) => $item !== $this->taskAttribute->name);
$extra = array_filter($input->getRawTokens(), fn ($item) => $item !== $this->taskAttribute->name);

$p = new Process([...$this->taskAttribute->console, $this->taskAttribute->originalName, ...$extra]);
$p->run(fn ($type, $bytes) => print ($bytes));
Expand Down
32 changes: 32 additions & 0 deletions src/Console/Command/TaskCommand.php
Expand Up @@ -5,8 +5,10 @@
use Castor\Attribute\AsArgument;
use Castor\Attribute\AsCommandArgument;
use Castor\Attribute\AsOption;
use Castor\Attribute\AsRawTokens;
use Castor\Attribute\AsTask;
use Castor\Console\Application;
use Castor\Console\Input\Input;
use Castor\Event\AfterExecuteTaskEvent;
use Castor\Event\BeforeExecuteTaskEvent;
use Castor\EventDispatcher;
Expand Down Expand Up @@ -76,7 +78,15 @@ public function isEnabled(): bool

protected function configure(): void
{
if ($this->taskAttribute->ignoreValidationErrors) {
$this->ignoreValidationErrors();
}

foreach ($this->function->getParameters() as $parameter) {
if ($parameter->getAttributes(AsRawTokens::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
continue;
}

$taskArgumentAttribute = $parameter->getAttributes(AsCommandArgument::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;

if ($taskArgumentAttribute) {
Expand Down Expand Up @@ -140,11 +150,33 @@ protected function configure(): void
}
}

/**
* @param Input $input
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$args = [];

foreach ($this->function->getParameters() as $parameter) {
if ($parameter->getAttributes(AsRawTokens::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
$parameters = [];
$keep = false;
foreach ($input->getRawTokens() as $value) {
if ($value === $input->getFirstArgument()) {
$keep = true;

continue;
}
if ($keep) {
$parameters[] = $value;
}
}

$args[] = $parameters;

continue;
}

$name = $this->getParameterName($parameter);
if ($input->hasArgument($name)) {
$args[] = $input->getArgument($name);
Expand Down
17 changes: 17 additions & 0 deletions src/Console/Input/Input.php
@@ -0,0 +1,17 @@
<?php

namespace Castor\Console\Input;

use Symfony\Component\Console\Input\ArgvInput;

class Input extends ArgvInput
{
/**
* @return list<string>
*/
public function getRawTokens(): array
{
// @phpstan-ignore-next-line
return (fn () => $this->tokens)->bindTo($this, ArgvInput::class)();
}
}
4 changes: 2 additions & 2 deletions src/GlobalHelper.php
Expand Up @@ -3,10 +3,10 @@
namespace Castor;

use Castor\Console\Application;
use Castor\Console\Input\Input;
use Monolog\Logger;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
Expand Down Expand Up @@ -57,7 +57,7 @@ public static function getLogger(): Logger
return self::getApplication()->logger;
}

public static function getInput(): InputInterface
public static function getInput(): Input
{
return self::getApplication()->getInput();
}
Expand Down
3 changes: 2 additions & 1 deletion src/functions.php
Expand Up @@ -4,6 +4,7 @@

use Castor\Attribute\AsContextGenerator;
use Castor\Console\Application;
use Castor\Console\Input\Input;
use Castor\Exception\ExecutableNotFoundException;
use Castor\Exception\MinimumVersionRequirementNotMetException;
use Castor\Exception\WaitFor\ExitedBeforeTimeoutException;
Expand Down Expand Up @@ -631,7 +632,7 @@ function get_application(): Application
return app();
}

function input(): InputInterface
function input(): Input
{
return GlobalHelper::getInput();
}
Expand Down
22 changes: 22 additions & 0 deletions tests/Examples/Generated/ArgPassthruExpanded.php
@@ -0,0 +1,22 @@
<?php

namespace Castor\Tests\Examples\Generated;

use Castor\Tests\TaskTestCase;

class ArgPassthruExpanded extends TaskTestCase
{
// args:passthru
public function test(): void
{
$process = $this->runTask(['args:passthru', 'a', 'b', '--no', '--foo', 'bar', '-x']);

$this->assertSame(0, $process->getExitCode());
$this->assertStringEqualsFile(__FILE__ . '.output.txt', $process->getOutput());
if (file_exists(__FILE__ . '.err.txt')) {
$this->assertStringEqualsFile(__FILE__ . '.err.txt', $process->getErrorOutput());
} else {
$this->assertSame('', $process->getErrorOutput());
}
}
}
14 changes: 14 additions & 0 deletions tests/Examples/Generated/ArgPassthruExpanded.php.output.txt
@@ -0,0 +1,14 @@
array(6) {
[0]=>
string(1) "a"
[1]=>
string(1) "b"
[2]=>
string(4) "--no"
[3]=>
string(5) "--foo"
[4]=>
string(3) "bar"
[5]=>
string(2) "-x"
}
22 changes: 22 additions & 0 deletions tests/Examples/Generated/ArgsPassthruTest.php
@@ -0,0 +1,22 @@
<?php

namespace Castor\Tests\Examples\Generated;

use Castor\Tests\TaskTestCase;

class ArgsPassthruTest extends TaskTestCase
{
// args:passthru
public function test(): void
{
$process = $this->runTask(['args:passthru']);

$this->assertSame(0, $process->getExitCode());
$this->assertStringEqualsFile(__FILE__ . '.output.txt', $process->getOutput());
if (file_exists(__FILE__ . '.err.txt')) {
$this->assertStringEqualsFile(__FILE__ . '.err.txt', $process->getErrorOutput());
} else {
$this->assertSame('', $process->getErrorOutput());
}
}
}
2 changes: 2 additions & 0 deletions tests/Examples/Generated/ArgsPassthruTest.php.output.txt
@@ -0,0 +1,2 @@
array(0) {
}
1 change: 1 addition & 0 deletions tests/Examples/Generated/ListTest.php.output.txt
Expand Up @@ -6,6 +6,7 @@ list List commands
no-namespace Task without a namespace
args:another-args Dumps all arguments and options, without configuration
args:args Dumps all arguments and options, with custom configuration
args:passthru Dumps all arguments and options, without configuration nor validation
bar:bar Prints bar, but also executes foo
cache:complex Cache with usage of CacheItemInterface
cache:simple Cache a simple call
Expand Down