Skip to content
Closed
7 changes: 7 additions & 0 deletions config/scribe.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@
'base_url' => null,
],

'from_tests' => [
/**
* Generate api documentation by running phpunit tests following "Test-Driven Documentation" principle.
*/
'enabled' => false,
],

/*
* How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls.
*/
Expand Down
86 changes: 86 additions & 0 deletions src/GroupedEndpoints/GroupedEndpointsAbstract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Knuckles\Scribe\GroupedEndpoints;

use Illuminate\Support\Str;
use Knuckles\Camel\Camel;
use Knuckles\Scribe\Commands\GenerateDocumentation;
use Knuckles\Scribe\Extracting\ApiDetails;
use Knuckles\Scribe\Matching\RouteMatcherInterface;
use Knuckles\Scribe\Tools\DocumentationConfig;
use Knuckles\Scribe\Tools\Utils;
use Symfony\Component\Yaml\Yaml;

abstract class GroupedEndpointsAbstract
{
protected GenerateDocumentation $command;
protected RouteMatcherInterface $routeMatcher;
protected DocumentationConfig $docConfig;

public static string $camelDir;
public static string $cacheDir;

public function __construct(GenerateDocumentation $command, RouteMatcherInterface $routeMatcher)
{
$this->command = $command;
$this->routeMatcher = $routeMatcher;
$this->docConfig = $command->getDocConfig();

static::$camelDir = GenerateDocumentation::$camelDir;
static::$cacheDir = GenerateDocumentation::$cacheDir;
}

public function get(): array
{
$groupedEndpoints = $this->extractEndpointsInfoAndWriteToDisk();
$this->extractAndWriteApiDetailsToDisk();

return $groupedEndpoints;
}

protected function extractAndWriteApiDetailsToDisk(): void
{
$apiDetails = new ApiDetails($this->docConfig, !$this->command->option('force'));
$apiDetails->writeMarkdownFiles();
}

protected function writeEndpointsToDisk(array $grouped): void
{
Utils::deleteFilesMatching(static::$camelDir, function (array $file) {
return !Str::startsWith($file['basename'], 'custom.');
});
Utils::deleteDirectoryAndContents(static::$cacheDir);

if (!is_dir(static::$camelDir)) {
mkdir(static::$camelDir, 0777, true);
}

if (!is_dir(static::$cacheDir)) {
mkdir(static::$cacheDir, 0777, true);
}

$fileNameIndex = 0;
foreach ($grouped as $group) {
$yaml = Yaml::dump(
$group,
20,
2,
Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK
);
if (
count(Camel::$groupFileNames) == count($grouped)
&& isset(Camel::$groupFileNames[$group['name']])
) {
$fileName = Camel::$groupFileNames[$group['name']];
} else {
$fileName = "$fileNameIndex.yaml";
$fileNameIndex++;
}

file_put_contents(static::$camelDir . "/$fileName", $yaml);
file_put_contents(static::$cacheDir . "/$fileName", "## Autogenerated by Scribe. DO NOT MODIFY.\n\n" . $yaml);
}
}

abstract protected function extractEndpointsInfoAndWriteToDisk(): array;
}
10 changes: 5 additions & 5 deletions src/GroupedEndpoints/GroupedEndpointsFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ class GroupedEndpointsFactory
{
public function make(GenerateDocumentation $command, RouteMatcherInterface $routeMatcher): GroupedEndpointsContract
{
if ($command->isForcing()) {
return new GroupedEndpointsFromApp($command, $routeMatcher, false);
if ($command->option('no-extraction')) {
return new GroupedEndpointsFromCamelDir;
}

if ($command->shouldExtract()) {
return new GroupedEndpointsFromApp($command, $routeMatcher, true);
if ($command->getDocConfig()->get('from_tests.enabled')) {
return new GroupedEndpointsFromTests($command, $routeMatcher);
}

return new GroupedEndpointsFromCamelDir;
return new GroupedEndpointsFromApp($command, $routeMatcher, !$command->isForcing());
}
}
57 changes: 6 additions & 51 deletions src/GroupedEndpoints/GroupedEndpointsFromApp.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,13 @@
use ReflectionClass;
use Symfony\Component\Yaml\Yaml;

class GroupedEndpointsFromApp implements GroupedEndpointsContract
class GroupedEndpointsFromApp extends GroupedEndpointsAbstract implements GroupedEndpointsContract
{
private GenerateDocumentation $command;
private RouteMatcherInterface $routeMatcher;
private DocumentationConfig $docConfig;
private bool $preserveUserChanges = true;
private bool $encounteredErrors = false;

public static string $camelDir;
public static string $cacheDir;

private array $endpointGroupIndexes = [];

public function __construct(GenerateDocumentation $command, RouteMatcherInterface $routeMatcher, $preserveUserChanges)
Expand All @@ -48,30 +44,29 @@ public function __construct(GenerateDocumentation $command, RouteMatcherInterfac

public function get(): array
{
$groupedEndpoints = $this->extractEndpointsInfoAndWriteToDisk($this->routeMatcher, $this->preserveUserChanges);
$this->extractAndWriteApiDetailsToDisk();
parent::__construct($command, $routeMatcher);

return $groupedEndpoints;
$this->preserveUserChanges = $preserveUserChanges;
}

public function hasEncounteredErrors(): bool
{
return $this->encounteredErrors;
}

protected function extractEndpointsInfoAndWriteToDisk(RouteMatcherInterface $routeMatcher, bool $preserveUserChanges): array
protected function extractEndpointsInfoAndWriteToDisk(): array
{
$latestEndpointsData = [];
$cachedEndpoints = [];
$groups = [];

if ($preserveUserChanges && is_dir(static::$camelDir) && is_dir(static::$cacheDir)) {
if ($this->preserveUserChanges && is_dir(static::$camelDir) && is_dir(static::$cacheDir)) {
$latestEndpointsData = Camel::loadEndpointsToFlatPrimitivesArray(static::$camelDir);
$cachedEndpoints = Camel::loadEndpointsToFlatPrimitivesArray(static::$cacheDir, true);
$groups = Camel::loadEndpointsIntoGroups(static::$camelDir);
}

$routes = $routeMatcher->getRoutes($this->docConfig->get('routes'), $this->docConfig->get('router'));
$routes = $this->routeMatcher->getRoutes($this->docConfig->get('routes'), $this->docConfig->get('router'));
$endpoints = $this->extractEndpointsInfoFromLaravelApp($routes, $cachedEndpoints, $latestEndpointsData, $groups);
$groupedEndpoints = Camel::groupEndpoints($endpoints, $this->endpointGroupIndexes);
$this->writeEndpointsToDisk($groupedEndpoints);
Expand Down Expand Up @@ -188,40 +183,6 @@ private function mergeAnyEndpointDataUpdates(ExtractedEndpointData $endpointData
return [$endpointData, $index];
}

protected function writeEndpointsToDisk(array $grouped): void
{
Utils::deleteFilesMatching(static::$camelDir, function (array $file) {
return !Str::startsWith($file['basename'], 'custom.');
});
Utils::deleteDirectoryAndContents(static::$cacheDir);

if (!is_dir(static::$camelDir)) {
mkdir(static::$camelDir, 0777, true);
}

if (!is_dir(static::$cacheDir)) {
mkdir(static::$cacheDir, 0777, true);
}

$fileNameIndex = 0;
foreach ($grouped as $group) {
$yaml = Yaml::dump(
$group, 20, 2,
Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK
);
if (count(Camel::$groupFileNames) == count($grouped)
&& isset(Camel::$groupFileNames[$group['name']])) {
$fileName = Camel::$groupFileNames[$group['name']];
} else {
$fileName = "$fileNameIndex.yaml";
$fileNameIndex++;
}

file_put_contents(static::$camelDir . "/$fileName", $yaml);
file_put_contents(static::$cacheDir . "/$fileName", "## Autogenerated by Scribe. DO NOT MODIFY.\n\n" . $yaml);
}
}

private function isValidRoute(array $routeControllerAndMethod = null): bool
{
if (is_array($routeControllerAndMethod)) {
Expand Down Expand Up @@ -277,10 +238,4 @@ protected function writeExampleCustomEndpoint(): void
copy(__DIR__ . '/../../resources/example_custom_endpoint.yaml', static::$camelDir . '/custom.0.yaml');
}
}

protected function extractAndWriteApiDetailsToDisk(): void
{
$apiDetails = new ApiDetails($this->docConfig, !$this->command->option('force'));
$apiDetails->writeMarkdownFiles();
}
}
34 changes: 34 additions & 0 deletions src/GroupedEndpoints/GroupedEndpointsFromTests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Knuckles\Scribe\GroupedEndpoints;

use Knuckles\Camel\Camel;
use Knuckles\Scribe\Commands\GenerateDocumentation;
use Knuckles\Scribe\Matching\RouteMatcherInterface;

class GroupedEndpointsFromTests extends GroupedEndpointsAbstract implements GroupedEndpointsContract
{
private bool $encounteredErrors = false;
private array $endpointGroupIndexes = [];

public function hasEncounteredErrors(): bool
{
return $this->encounteredErrors;
}

protected function extractEndpointsInfoAndWriteToDisk(): array
{
$routes = $this->routeMatcher->matchFromTestsOnly()->getRoutes($this->docConfig->get('routes'), $this->docConfig->get('router'));
$endpoints = $this->extractEndpointsInfoFromTests($routes);
$groupedEndpoints = Camel::groupEndpoints($endpoints, $this->endpointGroupIndexes);
$this->writeEndpointsToDisk($groupedEndpoints);
$groupedEndpoints = Camel::prepareGroupedEndpointsForOutput($groupedEndpoints);
return $groupedEndpoints;
}

private function extractEndpointsInfoFromTests(array $routes): array
{
// TODO: code
return [];
}
}
25 changes: 24 additions & 1 deletion src/Matching/RouteMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@

class RouteMatcher implements RouteMatcherInterface
{
private bool $matchFromTestsOnly = false;

public function matchFromTestsOnly(bool $bool = true): self
{
$this->matchFromTestsOnly = $bool;

return $this;
}

public function getRoutes(array $routeRules = [], string $router = 'laravel'): array
{
$usingDingoRouter = strtolower($router) == 'dingo';
Expand All @@ -34,7 +43,10 @@ private function getRoutesToBeDocumented(array $routeRules, bool $usingDingoRout
continue;
}

if ($this->shouldIncludeRoute($route, $routeRule, $includes, $usingDingoRouter)) {
if (
$this->shouldIncludeRoute($route, $routeRule, $includes, $usingDingoRouter)
&& $this->isMatchedFromTests($route)
) {
$matchedRoutes[] = new MatchedRoute($route, $routeRule['apply'] ?? []);
}
}
Expand Down Expand Up @@ -93,4 +105,15 @@ private function shouldExcludeRoute(Route $route, array $routeRule): bool
return Str::is($excludes, $route->getName())
|| Str::is($excludes, $route->uri());
}

private function isMatchedFromTests(Route $route): bool
{
if ($this->matchFromTestsOnly == false) {
return true;
}

// TODO: Run phpunit tests first to list all the routes that are included
// and check if route is included from tests
return true;
}
}