<?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 Chevere\DataStructure\Interfaces\MapInterface;
use Chevere\DataStructure\Interfaces\VectorInterface;
use Chevere\DataStructure\Map;
use Chevere\DataStructure\Traits\MapTrait;
use Chevere\DataStructure\Vector;
use Chevere\Parameter\Interfaces\BoolParameterInterface;
use Chevere\Parameter\Interfaces\MixedParameterInterface;
use Chevere\Parameter\Interfaces\ParameterInterface;
use Chevere\Parameter\Interfaces\ParametersAccessInterface;
use Chevere\Parameter\Interfaces\UnionParameterInterface;
use Chevere\Workflow\Exceptions\JobsException;
use Chevere\Workflow\Interfaces\GraphInterface;
use Chevere\Workflow\Interfaces\JobInterface;
use Chevere\Workflow\Interfaces\JobsInterface;
use Chevere\Workflow\Interfaces\ResponseReferenceInterface;
use Chevere\Workflow\Interfaces\VariableInterface;
use InvalidArgumentException;
use LogicException;
use OutOfBoundsException;
use OverflowException;
use Throwable;
use TypeError;
use function Chevere\Action\getParameters;
use function Chevere\Message\message;
use function Chevere\Parameter\bool;
final class Jobs implements JobsInterface
{
/**
* @template-use MapTrait<JobInterface>
*/
use MapTrait;
/**
* @var VectorInterface<string>
*/
private VectorInterface $jobs;
private GraphInterface $graph;
/**
* @var Map<ParameterInterface>
*/
private MapInterface $variables;
/**
* @var Map<ParameterInterface>
*/
private MapInterface $references;
/**
* @var VectorInterface<string>
*/
private VectorInterface $jobDependencies;
public function __construct(JobInterface ...$jobs)
{
$this->map = new Map();
$this->jobs = new Vector();
$this->graph = new Graph();
$this->variables = new Map();
$this->references = new Map();
$this->putAdded(...$jobs);
}
public function graph(): GraphInterface
{
return $this->graph;
}
public function variables(): MapInterface
{
return $this->variables;
}
public function references(): MapInterface
{
return $this->references;
}
public function get(string $job): JobInterface
{
/** @var JobInterface */
return $this->map->get($job);
}
public function has(string $job): bool
{
return $this->map->has($job);
}
public function withAdded(JobInterface ...$jobs): JobsInterface
{
$new = clone $this;
$new->putAdded(...$jobs);
return $new;
}
private function addMap(string $name, JobInterface $job): void
{
if ($this->map->has($name)) {
throw new OverflowException(
(string) message(
'Job name `%name%` has been already added.',
name: $name
)
);
}
$this->map = $this->map->withPut($name, $job);
}
private function putAdded(JobInterface ...$job): void
{
foreach ($job as $name => $item) {
$this->jobDependencies = $item->dependencies();
$name = strval($name);
$this->addMap($name, $item);
$this->jobs = $this->jobs->withPush($name);
$this->handleArguments($name, $item);
foreach ($item->runIf() as $runIf) {
$this->handleRunIfReference($runIf);
$this->handleRunIfVariable($name, $runIf);
}
$this->storeReferences($name, $item);
$this->assertDependencies($name);
$this->graph = $this->graph->withPut($name, $item);
}
}
private function storeReferences(string $job, JobInterface $item): void
{
$action = $item->action();
$return = $action::return();
if ($return instanceof ParametersAccessInterface
&& ! ($return instanceof UnionParameterInterface)
) {
foreach ($return->parameters() as $key => $parameter) {
$this->references = $this->references
->withPut(
strval(response($job, $key)),
$parameter,
);
}
} else {
$this->references = $this->references
->withPut(
strval(response($job)),
$return,
);
}
}
private function handleArguments(string $job, JobInterface $item): void
{
$errors = [];
foreach ($item->arguments() as $argument => $value) {
$action = $item->action();
$parameters = getParameters($action::class);
if ($parameters->has($argument)) {
$parameter = $parameters->get($argument);
} elseif ($parameters->isVariadic()) {
$lastKey = array_key_last($parameters->keys());
$lastName = $parameters->keys()[$lastKey];
$parameter = $parameters->get($lastName);
}
$collection = match (true) {
$value instanceof VariableInterface => 'variables',
$value instanceof ResponseReferenceInterface => 'references',
default => false
};
if (! $collection) {
continue;
}
try {
/**
* @var VariableInterface|ResponseReferenceInterface $value
* @phpstan-ignore-next-line
*/
$this->mapParameter($argument, $collection, $parameter, $value);
} catch (Throwable $e) {
throw new JobsException(
name: $job,
job: $item,
throwable: $e
);
}
}
}
private function mapParameter(
string $argument,
string $collection,
ParameterInterface $parameter,
VariableInterface|ResponseReferenceInterface $value,
): void {
/** @var MapInterface<ParameterInterface> $map */
$map = $this->{$collection};
$subject = 'Reference';
$identifier = strval($value);
if ($value instanceof VariableInterface) {
$subject = 'Variable';
} else {
try {
/** @var JobInterface $referenceJob */
$referenceJob = $this->map->get($value->job());
/** @var ParameterInterface $accept */
$accept = $referenceJob->action()::return();
if ($value->key() !== null) {
if (! $accept instanceof ParametersAccessInterface) {
throw new LogicException(
(string) message(
"Invalid reference **%reference%** as **%job%** doesn't return an object implementing %interface% interface",
job: $value->job(),
reference: strval($value),
interface: ParametersAccessInterface::class
)
);
}
$accept->parameters()->get($value->key());
}
} catch (OutOfBoundsException) {
throw new OutOfBoundsException(
(string) message(
'%subject% **%key%** not found',
subject: $subject,
key: $identifier
)
);
}
}
if (! $map->has($identifier)) {
$map = $map->withPut($identifier, $parameter);
$this->{$collection} = $map;
return;
}
/** @var ParameterInterface $stored */
$stored = $map->get($identifier);
if ($parameter instanceof UnionParameterInterface) {
foreach ($parameter->parameters() as $tryParameter) {
try {
$stored->assertCompatible($tryParameter);
$parameter = $tryParameter;
break;
} catch (TypeError $e) {
}
}
}
if ($stored instanceof MixedParameterInterface
|| $parameter instanceof MixedParameterInterface
) {
return; // @codeCoverageIgnore
}
if ($stored::class !== $parameter::class) {
throw new TypeError(
(string) message(
'%subject% **%key%** is of type `%type%`, parameter **%parameter%** expects `%expected%`',
parameter: $argument,
type: $stored->type()->primitive(),
expected: $parameter->type()->primitive(),
subject: $subject,
key: $identifier
)
);
}
try {
$stored->assertCompatible($parameter);
} catch (InvalidArgumentException $e) {
throw new InvalidArgumentException(
(string) message(
'%subject% **%key%** conflict for parameter **%parameter%** (%message%).',
subject: $subject,
key: $identifier,
parameter: $argument,
message: $e->getMessage()
)
);
}
}
private function handleRunIfReference(mixed $runIf): void
{
if (! $runIf instanceof ResponseReferenceInterface) {
return;
}
$action = $this->get($runIf->job())->action();
$accept = $action::return();
if ($runIf->key() !== null) {
if (! $accept instanceof ParametersAccessInterface) {
throw new OutOfBoundsException(
(string) message(
'Reference **%reference%** job `%job%` response doesn\'t bind to `%parameter%` parameter',
reference: strval($runIf),
job: $runIf->job(),
parameter: $runIf->key()
)
);
}
$accept = $accept->parameters()->get($runIf->key());
}
if ($accept->type()->primitive() === 'bool') {
return;
}
throw new TypeError(
(string) message(
'Reference **%reference%** must be of type `bool`, `%type%` provided',
reference: strval($runIf),
type: $accept->type()->primitive()
)
);
}
private function handleRunIfVariable(string $name, mixed $runIf): void
{
if (! $runIf instanceof VariableInterface) {
return;
}
if (! $this->variables->has($runIf->__toString())) {
$this->variables = $this->variables
->withPut(
$runIf->__toString(),
bool(),
);
return;
}
/** @var ParameterInterface $parameter */
$parameter = $this->variables->get($runIf->__toString());
if (! ($parameter instanceof BoolParameterInterface)) {
throw new TypeError(
(string) message(
'Variable **%variable%** (previously declared as `%type%`) is not of type `bool` at Job **%job%**',
variable: $runIf->__toString(),
type: $parameter->type()->primitive(),
job: $name,
)
);
}
}
private function assertDependencies(string $job): void
{
$dependencies = $this->jobDependencies->toArray();
if (! $this->jobs->contains(...$dependencies)) {
$missing = array_diff(
$dependencies,
$this->jobs->toArray()
);
throw new OutOfBoundsException(
(string) message(
'Job **%job%** has undeclared dependencies: `%dependencies%`',
job: $job,
dependencies: implode(', ', $missing),
)
);
}
}
}
|