chevere/chevere

View on GitHub
src/Spec/SpecMaker.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php

/*
 * This file is part of Chevere.
 *
 * (c) Rodolfo Berrios <rodolfo@chevere.org>
 *
 * 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\Spec;

use Chevere\Filesystem\Exceptions\FilesystemException;
use Chevere\Filesystem\File;
use Chevere\Filesystem\Interfaces\DirInterface;
use Chevere\Filesystem\Interfaces\PathInterface;
use Chevere\Message\Message;
use Chevere\Router\Interfaces\RouterInterface;
use Chevere\Spec\Interfaces\SpecIndexInterface;
use Chevere\Spec\Interfaces\SpecInterface;
use Chevere\Spec\Interfaces\SpecMakerInterface;
use Chevere\Spec\Interfaces\Specs\IndexSpecInterface;
use Chevere\Spec\Specs\GroupSpec;
use Chevere\Spec\Specs\IndexSpec;
use Chevere\Spec\Specs\RouteSpec;
use Chevere\Str\Str;
use Chevere\Throwable\Exception;
use Chevere\Throwable\Exceptions\InvalidArgumentException;
use Ds\Map;
use Throwable;

final class SpecMaker implements SpecMakerInterface
{
    private SpecIndexInterface $specIndex;

    private IndexSpecInterface $indexSpec;

    private Map $files;

    public function __construct(
        private DirInterface $specDir,
        private DirInterface $outputDir,
        private RouterInterface $router
    ) {
        $this->assertDir();
        $this->assertRouter();
        $this->specIndex = new SpecIndex();
        $this->indexSpec = new IndexSpec($this->specDir);
        $this->files = new Map();
        $routes = $router->routes();
        $groups = [];
        foreach ($routes->getIterator() as $routeName => $routable) {
            $repository = $router->index()->getRouteGroup($routeName);
            if (!isset($groups[$repository])) {
                $groups[$repository] = new GroupSpec($specDir, $repository);
            }
            $routableSpec = new RouteSpec(
                $specDir->getChild("${repository}/"),
                $routable,
                $repository
            );
            $this->makeJsonFile($routableSpec);
            /** @var GroupSpec $groupSpec */
            $groupSpec = $groups[$repository];
            $groupSpec = $groupSpec->withAddedRoutableSpec($routableSpec);
            $groups[$repository] = $groupSpec;
            $routeEndpointSpecs = $routableSpec->clonedRouteEndpointSpecs();
            foreach ($routeEndpointSpecs->getIterator() as $routeEndpointSpec) {
                $this->specIndex = $this->specIndex->withAddedRoute(
                    $routeName,
                    $routeEndpointSpec
                );
                $this->makeJsonFile($routeEndpointSpec);
            }
        }
        foreach ($groups as $groupSpec) {
            $this->makeJsonFile($groupSpec);
            $this->indexSpec = $this->indexSpec->withAddedGroup($groupSpec);
        }
        $this->makeJsonFile($this->indexSpec);
    }

    public function specIndex(): SpecIndexInterface
    {
        return $this->specIndex;
    }

    public function files(): Map
    {
        return $this->files->copy();
    }

    /**
     * @codeCoverageIgnore
     */
    private function assertDir(): void
    {
        try {
            if (!$this->outputDir->exists()) {
                $this->outputDir->create(0755);
            }
            $this->outputDir->assertExists();
            if (!$this->outputDir->path()->isWritable()) {
                throw new Exception(
                    (new Message('Directory %pathName% is not writable'))
                        ->code('%pathName%', $this->outputDir->path()->__toString())
                );
            }
            $this->outputDir->removeContents();
        } catch (Throwable $e) {
            throw new FilesystemException(previous: $e);
        }
    }

    private function assertRouter(): void
    {
        if (count($this->router->routes()) === 0) {
            throw new InvalidArgumentException(
                (new Message('Instance of %interfaceName% does not contain any routable.'))
                    ->code('%interfaceName%', RouterInterface::class)
            );
        }
    }

    private function makeJsonFile(SpecInterface $spec): void
    {
        $filePath = $this->getPathFor($spec->jsonPath());
        $this->files->put($spec->jsonPath(), $filePath);

        try {
            $file = new File($filePath);
            if ($file->exists()) {
                // @codeCoverageIgnoreStart
                $file->remove();
                // @codeCoverageIgnoreEnd
            }
            $file->create();
            $file->put($this->toJson($spec->toArray()));
        }
        // @codeCoverageIgnoreStart
        catch (Throwable $e) {
            throw new FilesystemException(
                previous: $e,
                message: (new Message('Unable to make file %filename%'))
                    ->code('%filename%', $filePath->__toString()),
            );
        }
        // @codeCoverageIgnoreEnd
    }

    private function getPathFor(string $jsonPath): PathInterface
    {
        try {
            $dirPath = $this->outputDir->path();
            $child = (new Str($jsonPath))
                ->withReplaceFirst($this->specDir->path()->__toString(), '')
                ->__toString();
            $child = ltrim($child, '/');

            return $dirPath->getChild($child);
        }
        // @codeCoverageIgnoreStart
        catch (Throwable $e) {
            throw new FilesystemException(
                previous: $e,
                message: (new Message('Unable to retrieve path for %argument%'))
                    ->code('%argument%', $jsonPath),
            );
        }
        // @codeCoverageIgnoreEnd
    }

    private function toJson(array $array): string
    {
        return json_encode($array, JSON_PRETTY_PRINT | JSON_FORCE_OBJECT);
    }
}