<?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\VectorInterface;
use Chevere\DataStructure\Map;
use Chevere\DataStructure\Traits\MapTrait;
use Chevere\DataStructure\Vector;
use Chevere\Workflow\Interfaces\GraphInterface;
use Chevere\Workflow\Interfaces\JobInterface;
use InvalidArgumentException;
use function Chevere\Message\message;
final class Graph implements GraphInterface
{
/**
* @template-use MapTrait<VectorInterface<string>>
*/
use MapTrait;
/**
* @var VectorInterface<string>
*/
private VectorInterface $syncJobs;
public function __construct()
{
$this->map = new Map();
$this->syncJobs = new Vector();
}
public function withPut(
string $name,
JobInterface $job,
): GraphInterface {
$vector = $job->dependencies();
$this->assertNotSelfDependency($name, $vector);
$new = clone $this;
foreach ($vector as $dependency) {
if (! $new->has($dependency)) {
$new->map = $new->map
->withPut($dependency, new Vector());
}
}
if ($new->map->has($name)) {
/** @var VectorInterface<string> $existing */
$existing = $new->map->get($name);
$merge = array_merge($existing->toArray(), $vector->toArray());
$vector = new Vector(...$merge);
}
$new->handleDependencyUpdate($name, $vector);
$new->map = $new->map->withPut($name, $vector);
if ($job->isSync()) {
$new->syncJobs = $new->syncJobs->withPush($name);
}
return $new;
}
public function has(string $job): bool
{
return $this->map->has($job);
}
public function get(string $job): VectorInterface
{
/** @var VectorInterface<string> */
return $this->map->get($job);
}
public function hasDependencies(string $job, string ...$dependencies): bool
{
/** @var VectorInterface<string> $array */
$array = $this->map->get($job);
return $array->contains(...$dependencies);
}
public function toArray(): array
{
$sort = [];
$previous = [];
$sync = [];
$toIndex = 0;
foreach ($this->getSortAsc() as $job => $dependencies) {
foreach ($dependencies as $dependency) {
if (in_array($dependency, $previous, true)) {
$toIndex++;
$previous = [];
break;
}
}
$sort[$toIndex][] = $job;
$previous[] = $job;
if ($this->syncJobs->find($job) !== null) {
$sync[$job] = $toIndex;
}
}
return $this->getSortJobs($sort, $sync);
}
/**
* @return array<string, VectorInterface<string>>
*/
private function getSortAsc(): array
{
$array = $this->map->toArray();
uasort($array, function (VectorInterface $a, VectorInterface $b) {
return match (true) {
$b->contains(...$a->toArray()) => -1,
// @infection-ignore-all
$a->contains(...$b->toArray()) => 1,
default => 0
};
});
/* @phpstan-ignore-next-line */
return $array;
}
/**
* @param array<int, array<int, string>> $sort
* @param array<string, int> $sync
* @return array<int, array<int, string>>
*/
private function getSortJobs(array $sort, array $sync): array
{
if (count($this->syncJobs) === 0) {
return $sort;
}
$aux = 0;
$vector = new Vector(...$sort);
foreach ($sync as $job => $index) {
$auxIndex = $index + $aux;
/** @var array<int, string> $array */
$array = $vector->get($auxIndex);
$key = array_search($job, $array, true);
unset($array[$key]);
$array = array_values($array);
$vector = $vector
->withSet($auxIndex, $array)
->withInsert($auxIndex, [$job]);
$aux++;
}
/** @var array<int, array<int, string>> */
$array = $vector->toArray();
return array_values(
array_filter($array)
);
}
/**
* @param VectorInterface<string> $vector
*/
private function assertNotSelfDependency(string $job, VectorInterface $vector): void
{
if (! $vector->contains($job)) {
return;
}
throw new InvalidArgumentException(
(string) message(
'Cannot declare job **%job%** as a self-dependency',
job: $job
)
);
}
/**
* @param VectorInterface<string> $vector
*/
private function handleDependencyUpdate(string $job, VectorInterface $vector): void
{
/** @var string $dependency */
foreach ($vector as $dependency) {
/** @var VectorInterface<string> $update */
$update = $this->map->get($dependency);
$findJob = $update->find($job);
if ($findJob !== null) {
$update = $update->withRemove($findJob);
}
$this->map = $this->map->withPut($dependency, $update);
}
}
}
|