<?php 
 
/* 
 * This file is part of Chevere. 
 * 
 * (c) Rodolfo Berrios <[email protected]> 
 * 
 * For the full copyright and license information, please view the LICENSE 
 * file that was distributed with this source code. 
 */ 
 
declare(strict_types=1); 
 
namespace Chevere\Workflow; 
 
use ArgumentCountError; 
use Chevere\Action\Interfaces\ActionInterface; 
use Chevere\DataStructure\Interfaces\VectorInterface; 
use Chevere\DataStructure\Vector; 
use Chevere\Parameter\Interfaces\ParameterInterface; 
use Chevere\Parameter\Interfaces\ParametersInterface; 
use Chevere\Workflow\Interfaces\CallerInterface; 
use Chevere\Workflow\Interfaces\JobInterface; 
use Chevere\Workflow\Interfaces\ResponseReferenceInterface; 
use Chevere\Workflow\Interfaces\VariableInterface; 
use InvalidArgumentException; 
use OverflowException; 
use ReflectionClass; 
use function Chevere\Action\getParameters; 
use function Chevere\Message\message; 
use function Chevere\Parameter\assertNamedArgument; 
 
final class Job implements JobInterface 
{ 
    /** 
     * @var array<string, mixed> 
     */ 
    private array $arguments; 
 
    /** 
     * @var VectorInterface<string> 
     */ 
    private VectorInterface $dependencies; 
 
    private ParametersInterface $parameters; 
 
    /** 
     * @var VectorInterface<ResponseReferenceInterface|VariableInterface> 
     */ 
    private VectorInterface $runIf; 
 
    private bool $isSync; 
 
    private CallerInterface $caller; 
 
    /** 
     * Creates a Job 
     * DO NOT use this method directly, use `sync` or `async` functions instead. 
     * 
     * @param ActionInterface $_ The action to run 
     * @param mixed ...$argument Action arguments for its run method (raw, reference or variable) 
     */ 
    public function __construct( 
        private ActionInterface $_, 
        mixed ...$argument 
    ) { 
        $debugBacktrace = debug_backtrace(options: 0, limit: 2); 
        $callerFunction = $debugBacktrace[1]['function'] ?? ''; 
        $index = (int) in_array( 
            $callerFunction, 
            ['Chevere\Workflow\sync', 'Chevere\Workflow\async'] 
        ); 
        $debugBacktrace = $debugBacktrace[$index]; 
        $file = $debugBacktrace['file'] ?? 'unknown'; 
        $line = $debugBacktrace['line'] ?? 0; 
        $this->caller = new Caller($file, (int) $line); 
        $this->isSync = false; 
        $this->runIf = new Vector(); 
        $this->dependencies = new Vector(); 
        $this->parameters = getParameters($_::class); 
        $this->arguments = []; 
        $this->setArguments(...$argument); 
    } 
 
    public function caller(): CallerInterface 
    { 
        return $this->caller; 
    } 
 
    public function withArguments(mixed ...$argument): JobInterface 
    { 
        $new = clone $this; 
        $new->setArguments(...$argument); 
 
        return $new; 
    } 
 
    public function withRunIf(ResponseReferenceInterface|VariableInterface ...$context): JobInterface 
    { 
        $new = clone $this; 
        $new->runIf = new Vector(); 
        $known = new Vector(); 
        foreach ($context as $item) { 
            if ($known->contains($item->__toString())) { 
                throw new OverflowException( 
                    (string) message( 
                        'Condition `%condition%` is already defined', 
                        condition: $item->__toString() 
                    ) 
                ); 
            } 
            $new->inferDependencies($item); 
            $new->runIf = $new->runIf->withPush($item); 
            $known = $known->withPush($item->__toString()); 
        } 
 
        return $new; 
    } 
 
    public function withIsSync(bool $flag = true): JobInterface 
    { 
        $new = clone $this; 
        $new->isSync = $flag; 
 
        return $new; 
    } 
 
    public function withDepends(string ...$jobs): JobInterface 
    { 
        $new = clone $this; 
        $new->addDependencies(...$jobs); 
 
        return $new; 
    } 
 
    public function action(): ActionInterface 
    { 
        return $this->_; 
    } 
 
    public function arguments(): array 
    { 
        return $this->arguments; 
    } 
 
    public function dependencies(): VectorInterface 
    { 
        return $this->dependencies; 
    } 
 
    public function runIf(): VectorInterface 
    { 
        return $this->runIf; 
    } 
 
    public function isSync(): bool 
    { 
        return $this->isSync; 
    } 
 
    private function setArguments(mixed ...$argument): void 
    { 
        if (! $this->parameters->isVariadic()) { 
            $this->assertArgumentsCount($argument); 
        } 
        $lastKey = array_key_last($this->parameters->keys()); 
        $lastName = $this->parameters->keys()[$lastKey] ?? null; 
        $values = []; 
        foreach ($this->parameters as $name => $parameter) { 
            if ($name === $lastName && $this->parameters->isVariadic()) { 
                $variadicKeys = array_diff_key( 
                    $argument, 
                    array_flip($this->parameters->keys()) 
                ); 
                foreach ($variadicKeys as $key => $value) { 
                    $key = strval($key); 
                    $values[$key] = $value; 
                    $this->inferDependencies($value); 
                    $this->assertParameter($name, $parameter, $value); 
                } 
 
                break; 
            } 
 
            if (array_key_exists($name, $argument)) { 
                $value = $argument[$name]; 
                $values[$name] = $value; 
                $this->inferDependencies($value); 
                $this->assertParameter($name, $parameter, $value); 
            } 
        } 
        $this->arguments = $values; 
    } 
 
    /** 
     * @param mixed[] $arguments 
     */ 
    private function assertArgumentsCount(array $arguments): void 
    { 
        $countProvided = count($arguments); 
        $requiredKeys = $this->parameters->requiredKeys()->toArray(); 
        $intersectKeys = array_intersect(array_keys($arguments), $requiredKeys); 
        $countIntersect = count($intersectKeys); 
        $missing = array_map( 
            fn (string $item) => $this->formatAsVariable($item), 
            array_diff($requiredKeys, $intersectKeys) 
        ); 
        if ($missing !== []) { 
            $reflection = new ReflectionClass($this->_); 
            $class = "`{$reflection->getName()}`"; 
            if ($reflection->isAnonymous()) { 
                $class = 'anon class in ' 
                    . $reflection->getFileName() . ':' . $reflection->getStartLine(); 
            } 
 
            throw new ArgumentCountError( 
                (string) message( 
                    'Missing argument(s) [`%arguments%`] for %action%', 
                    arguments: implode(', ', $missing), 
                    action: $class 
                ) 
            ); 
        } 
        if (count($requiredKeys) > $countProvided 
            || count($requiredKeys) !== $countIntersect 
            || $countProvided > count($this->parameters) 
        ) { 
            $requiredVars = array_map( 
                fn (string $item) => $this->formatAsVariable($item), 
                $requiredKeys 
            ); 
            $parameters = implode(', ', $requiredVars); 
            $parameters = $parameters === '' ? '' : "[{$parameters}]"; 
 
            throw new ArgumentCountError( 
                (string) message( 
                    '`%symbol%` requires %countRequired% argument(s)%parameters%', 
                    symbol: $this->_::class . '::' . $this->_::mainMethod(), 
                    countRequired: strval(count($requiredKeys)), 
                    parameters: $parameters === '' ? '' : " `{$parameters}`" 
                ) 
            ); 
        } 
    } 
 
    private function formatAsVariable(string $name): string 
    { 
        return $this->parameters->get($name)->type()->typeHinting() 
            . " \${$name}"; 
    } 
 
    private function assertParameter(string $name, ParameterInterface $parameter, mixed $value): void 
    { 
        if ($value instanceof ResponseReferenceInterface || $value instanceof VariableInterface) { 
            return; 
        } 
        assertNamedArgument($name, $parameter, $value); 
    } 
 
    private function inferDependencies(mixed $argument): void 
    { 
        $condition = $argument instanceof ResponseReferenceInterface; 
        if (! $condition) { 
            return; 
        } 
        if ($this->dependencies->contains($argument->job())) { 
            return; 
        } 
        $this->dependencies = $this->dependencies 
            ->withPush($argument->job()); 
    } 
 
    private function addDependencies(string ...$jobs): void 
    { 
        $this->assertDependencies(...$jobs); 
        foreach ($jobs as $job) { 
            if ($this->dependencies->contains($job)) { 
                continue; 
            } 
            $this->dependencies = $this->dependencies->withPush($job); 
        } 
    } 
 
    private function assertDependencies(string ...$dependencies): void 
    { 
        $uniques = array_unique($dependencies); 
        if ($uniques !== $dependencies) { 
            throw new OverflowException( 
                (string) message( 
                    'Job dependencies must be unique (repeated **%dependencies%**)', 
                    dependencies: implode(', ', array_diff_assoc($dependencies, $uniques)) 
                ) 
            ); 
        } 
        foreach ($dependencies as $dependency) { 
            if (empty($dependency) || ctype_digit($dependency) || ctype_space($dependency)) { 
                throw new InvalidArgumentException(); 
            } 
        } 
    } 
} 
 
 |