Browse Source

Merge pull request #263 from ajcastro/refactor

Separate the concern of getting groupedEndpoints by creating different strategies
Shalvah 4 years ago
parent
commit
30e485010f

+ 17 - 241
src/Commands/GenerateDocumentation.php

@@ -6,23 +6,19 @@ use Illuminate\Console\Command;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\URL;
 use Illuminate\Support\Str;
+use Knuckles\Camel\Camel;
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Knuckles\Camel\Output\OutputEndpointData;
-use Knuckles\Camel\Camel;
-use Knuckles\Scribe\Extracting\Extractor;
 use Knuckles\Scribe\Extracting\ApiDetails;
-use Knuckles\Scribe\Matching\MatchedRoute;
+use Knuckles\Scribe\Extracting\Extractor;
+use Knuckles\Scribe\GroupedEndpoints\GroupedEndpointsFactory;
 use Knuckles\Scribe\Matching\RouteMatcherInterface;
 use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
 use Knuckles\Scribe\Tools\DocumentationConfig;
 use Knuckles\Scribe\Tools\ErrorHandlingUtils as e;
 use Knuckles\Scribe\Tools\Globals;
 use Knuckles\Scribe\Tools\Utils;
-use Knuckles\Scribe\Tools\Utils as u;
 use Knuckles\Scribe\Writing\Writer;
-use Mpociot\Reflection\DocBlock;
-use Mpociot\Reflection\DocBlock\Tag;
-use ReflectionClass;
 use Symfony\Component\Yaml\Yaml;
 
 class GenerateDocumentation extends Command
@@ -42,191 +38,40 @@ class GenerateDocumentation extends Command
     private bool $shouldExtract;
 
     private bool $forcing;
-    private bool $encounteredErrors = false;
-    private array $endpointGroupIndexes = [];
 
-    public function handle(RouteMatcherInterface $routeMatcher): void
+    public function handle(RouteMatcherInterface $routeMatcher, GroupedEndpointsFactory $groupedEndpointsFactory): void
     {
         $this->bootstrap();
 
-        if ($this->forcing) {
-            $groupedEndpoints = $this->extractEndpointsInfoAndWriteToDisk($routeMatcher, false);
-            $this->extractAndWriteApiDetailsToDisk();
-        } else if ($this->shouldExtract) {
-            $groupedEndpoints = $this->extractEndpointsInfoAndWriteToDisk($routeMatcher, true);
-            $this->extractAndWriteApiDetailsToDisk();
-        } else {
-            if (!is_dir(static::$camelDir)) {
-                throw new \InvalidArgumentException("Can't use --no-extraction because there are no endpoints in the " . static::$camelDir . " directory.");
-            }
-            $groupedEndpoints = Camel::loadEndpointsIntoGroups(static::$camelDir);
-        }
+        $groupedEndpointsInstance = $groupedEndpointsFactory->make($this, $routeMatcher);
+
+        $groupedEndpoints = $this->mergeUserDefinedEndpoints(
+            $groupedEndpointsInstance->get(),
+            Camel::loadUserDefinedEndpoints(static::$camelDir)
+        );
 
-        $userDefinedEndpoints = Camel::loadUserDefinedEndpoints(static::$camelDir);
-        $groupedEndpoints = $this->mergeUserDefinedEndpoints($groupedEndpoints, $userDefinedEndpoints);
         $writer = new Writer($this->docConfig);
         $writer->writeDocs($groupedEndpoints);
 
-        if ($this->encounteredErrors) {
+        if ($groupedEndpointsInstance->hasEncounteredErrors()) {
             c::warn('Generated docs, but encountered some errors while processing routes.');
             c::warn('Check the output above for details.');
         }
     }
 
-    /**
-     * @param MatchedRoute[] $matches
-     * @param array $cachedEndpoints
-     * @param array $latestEndpointsData
-     * @param array[] $groups
-     *
-     * @return array
-     * @throws \Exception
-     */
-    private function extractEndpointsInfoFromLaravelApp(array $matches, array $cachedEndpoints, array $latestEndpointsData, array $groups): array
-    {
-        $generator = new Extractor($this->docConfig);
-        $parsedEndpoints = [];
-
-        foreach ($matches as $routeItem) {
-            $route = $routeItem->getRoute();
-
-            $routeControllerAndMethod = u::getRouteClassAndMethodNames($route);
-            if (!$this->isValidRoute($routeControllerAndMethod)) {
-                c::warn('Skipping invalid route: ' . c::getRouteRepresentation($route));
-                continue;
-            }
-
-            if (!$this->doesControllerMethodExist($routeControllerAndMethod)) {
-                c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ' - Controller method does not exist.');
-                continue;
-            }
-
-            if ($this->isRouteHiddenFromDocumentation($routeControllerAndMethod)) {
-                c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ': @hideFromAPIDocumentation was specified.');
-                continue;
-            }
-
-            try {
-                c::info('Processing route: ' . c::getRouteRepresentation($route));
-                $currentEndpointData = $generator->processRoute($route, $routeItem->getRules());
-                // If latest data is different from cached data, merge latest into current
-                [$currentEndpointData, $index] = $this->mergeAnyEndpointDataUpdates($currentEndpointData, $cachedEndpoints, $latestEndpointsData, $groups);
-
-                // We need to preserve order of endpoints, in case user did custom sorting
-                $parsedEndpoints[] = $currentEndpointData;
-                if ($index !== null) {
-                    $this->endpointGroupIndexes[$currentEndpointData->endpointId()] = $index;
-                }
-                c::success('Processed route: ' . c::getRouteRepresentation($route));
-            } catch (\Exception $exception) {
-                $this->encounteredErrors = true;
-                c::error('Failed processing route: ' . c::getRouteRepresentation($route) . ' - Exception encountered.');
-                e::dumpExceptionIfVerbose($exception);
-            }
-        }
-
-        return $parsedEndpoints;
-    }
-
-    /**
-     * @param ExtractedEndpointData $endpointData
-     * @param array[] $cachedEndpoints
-     * @param array[] $latestEndpointsData
-     * @param array[] $groups
-     *
-     * @return array The extracted endpoint data and the endpoint's index in the group file
-     */
-    private function mergeAnyEndpointDataUpdates(ExtractedEndpointData $endpointData, array $cachedEndpoints, array $latestEndpointsData, array $groups): array
-    {
-        // First, find the corresponding endpoint in cached and latest
-        $thisEndpointCached = Arr::first($cachedEndpoints, function (array $endpoint) use ($endpointData) {
-            return $endpoint['uri'] === $endpointData->uri && $endpoint['httpMethods'] === $endpointData->httpMethods;
-        });
-        if (!$thisEndpointCached) {
-            return [$endpointData, null];
-        }
-
-        $thisEndpointLatest = Arr::first($latestEndpointsData, function (array $endpoint) use ($endpointData) {
-            return $endpoint['uri'] === $endpointData->uri && $endpoint['httpMethods'] == $endpointData->httpMethods;
-        });
-        if (!$thisEndpointLatest) {
-            return [$endpointData, null];
-        }
-
-        // Then compare cached and latest to see what sections changed.
-        $properties = [
-            'metadata',
-            'headers',
-            'urlParameters',
-            'queryParameters',
-            'bodyParameters',
-            'responses',
-            'responseFields',
-        ];
-
-        $changed = [];
-        foreach ($properties as $property) {
-            if ($thisEndpointCached[$property] != $thisEndpointLatest[$property]) {
-                $changed[] = $property;
-            }
-        }
-
-        // Finally, merge any changed sections.
-        $thisEndpointLatest = OutputEndpointData::create($thisEndpointLatest);
-        foreach ($changed as $property) {
-            $endpointData->$property = $thisEndpointLatest->$property;
-        }
-        $index = Camel::getEndpointIndexInGroup($groups, $thisEndpointLatest);
-
-        return [$endpointData, $index];
-    }
-
-    private function isValidRoute(array $routeControllerAndMethod = null): bool
+    public function isForcing(): bool
     {
-        if (is_array($routeControllerAndMethod)) {
-            [$classOrObject, $method] = $routeControllerAndMethod;
-            if (u::isInvokableObject($classOrObject)) {
-                return true;
-            }
-            $routeControllerAndMethod = $classOrObject . '@' . $method;
-        }
-
-        return !is_callable($routeControllerAndMethod) && !is_null($routeControllerAndMethod);
+        return $this->forcing;
     }
 
-    private function doesControllerMethodExist(array $routeControllerAndMethod): bool
+    public function shouldExtract(): bool
     {
-        [$class, $method] = $routeControllerAndMethod;
-        $reflection = new ReflectionClass($class);
-
-        if ($reflection->hasMethod($method)) {
-            return true;
-        }
-
-        return false;
+        return $this->shouldExtract;
     }
 
-    private function isRouteHiddenFromDocumentation(array $routeControllerAndMethod): bool
+    public function getDocConfig()
     {
-        if (!($class = $routeControllerAndMethod[0]) instanceof \Closure) {
-            $classDocBlock = new DocBlock((new ReflectionClass($class))->getDocComment() ?: '');
-            $shouldIgnoreClass = collect($classDocBlock->getTags())
-                ->filter(function (Tag $tag) {
-                    return Str::lower($tag->getName()) === 'hidefromapidocumentation';
-                })->isNotEmpty();
-
-            if ($shouldIgnoreClass) {
-                return true;
-            }
-        }
-
-        $methodDocBlock = new DocBlock(u::getReflectedRouteMethod($routeControllerAndMethod)->getDocComment() ?: '');
-        $shouldIgnoreMethod = collect($methodDocBlock->getTags())
-            ->filter(function (Tag $tag) {
-                return Str::lower($tag->getName()) === 'hidefromapidocumentation';
-            })->isNotEmpty();
-
-        return $shouldIgnoreMethod;
+        return $this->docConfig;
     }
 
     public function bootstrap(): void
@@ -252,40 +97,6 @@ class GenerateDocumentation extends Command
         Camel::$groupFileNames = [];
     }
 
-    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);
-        }
-    }
-
     protected function mergeUserDefinedEndpoints(array $groupedEndpoints, array $userDefinedEndpoints): array
     {
         foreach ($userDefinedEndpoints as $endpoint) {
@@ -307,39 +118,4 @@ class GenerateDocumentation extends Command
 
         return $groupedEndpoints;
     }
-
-    protected function extractEndpointsInfoAndWriteToDisk(RouteMatcherInterface $routeMatcher, bool $preserveUserChanges): array
-    {
-        $latestEndpointsData = [];
-        $cachedEndpoints = [];
-        $groups = [];
-
-        if ($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'));
-        $endpoints = $this->extractEndpointsInfoFromLaravelApp($routes, $cachedEndpoints, $latestEndpointsData, $groups);
-        $groupedEndpoints = Camel::groupEndpoints($endpoints, $this->endpointGroupIndexes);
-        $this->writeEndpointsToDisk($groupedEndpoints);
-        $this->writeExampleCustomEndpoint();
-        $groupedEndpoints = Camel::prepareGroupedEndpointsForOutput($groupedEndpoints);
-        return $groupedEndpoints;
-    }
-
-    protected function writeExampleCustomEndpoint(): void
-    {
-        // We add an example to guide users in case they need to add a custom endpoint.
-        if (!file_exists(static::$camelDir . '/custom.0.yaml')) {
-            copy(__DIR__ . '/../../resources/example_custom_endpoint.yaml', static::$camelDir . '/custom.0.yaml');
-        }
-    }
-
-    protected function extractAndWriteApiDetailsToDisk(): void
-    {
-        $apiDetails = new ApiDetails($this->docConfig, !$this->option('force'));
-        $apiDetails->writeMarkdownFiles();
-    }
 }

+ 10 - 0
src/GroupedEndpoints/GroupedEndpointsContract.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace Knuckles\Scribe\GroupedEndpoints;
+
+interface GroupedEndpointsContract
+{
+    public function get(): array;
+
+    public function hasEncounteredErrors(): bool;
+}

+ 23 - 0
src/GroupedEndpoints/GroupedEndpointsFactory.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Knuckles\Scribe\GroupedEndpoints;
+
+use Knuckles\Camel\Camel;
+use Knuckles\Scribe\Commands\GenerateDocumentation;
+use Knuckles\Scribe\Matching\RouteMatcherInterface;
+
+class GroupedEndpointsFactory
+{
+    public static function make(GenerateDocumentation $command, RouteMatcherInterface $routeMatcher): GroupedEndpointsContract
+    {
+        if ($command->isForcing()) {
+            return new GroupedEndpointsFromApp($command, $routeMatcher, false);
+        }
+
+        if ($command->shouldExtract()) {
+            return new GroupedEndpointsFromApp($command, $routeMatcher, true);
+        }
+
+        return new GroupedEndpointsFromCamelDir;
+    }
+}

+ 285 - 0
src/GroupedEndpoints/GroupedEndpointsFromApp.php

@@ -0,0 +1,285 @@
+<?php
+
+namespace Knuckles\Scribe\GroupedEndpoints;
+
+use Illuminate\Support\Arr;
+use Illuminate\Support\Str;
+use Knuckles\Camel\Camel;
+use Knuckles\Camel\Extraction\ExtractedEndpointData;
+use Knuckles\Camel\Output\OutputEndpointData;
+use Knuckles\Scribe\Commands\GenerateDocumentation;
+use Knuckles\Scribe\Extracting\ApiDetails;
+use Knuckles\Scribe\Extracting\Extractor;
+use Knuckles\Scribe\Matching\MatchedRoute;
+use Knuckles\Scribe\Matching\RouteMatcherInterface;
+use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
+use Knuckles\Scribe\Tools\ErrorHandlingUtils as e;
+use Knuckles\Scribe\Tools\Utils as u;
+use Knuckles\Scribe\Tools\Utils;
+use Mpociot\Reflection\DocBlock;
+use Mpociot\Reflection\DocBlock\Tag;
+use ReflectionClass;
+use Symfony\Component\Yaml\Yaml;
+
+class GroupedEndpointsFromApp implements GroupedEndpointsContract
+{
+    private $command;
+    private $routeMatcher;
+    private $docConfig;
+    private $preserveUserChanges;
+    private bool $encounteredErrors = false;
+
+    public static string $camelDir;
+    public static string $cacheDir;
+
+    private array $endpointGroupIndexes = [];
+
+    public function __construct(GenerateDocumentation $command, RouteMatcherInterface $routeMatcher, $preserveUserChanges)
+    {
+        $this->command = $command;
+        $this->routeMatcher = $routeMatcher;
+        $this->docConfig = $command->getDocConfig();
+        $this->preserveUserChanges = $preserveUserChanges;
+
+        static::$camelDir = GenerateDocumentation::$camelDir;
+        static::$cacheDir = GenerateDocumentation::$cacheDir;
+    }
+
+    public function get(): array
+    {
+        $groupedEndpoints = $this->extractEndpointsInfoAndWriteToDisk($this->routeMatcher, $this->preserveUserChanges);
+        $this->extractAndWriteApiDetailsToDisk();
+
+        return $groupedEndpoints;
+    }
+
+    public function hasEncounteredErrors(): bool
+    {
+        return $this->encounteredErrors;
+    }
+
+    protected function extractEndpointsInfoAndWriteToDisk(RouteMatcherInterface $routeMatcher, bool $preserveUserChanges): array
+    {
+        $latestEndpointsData = [];
+        $cachedEndpoints = [];
+        $groups = [];
+
+        if ($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'));
+        $endpoints = $this->extractEndpointsInfoFromLaravelApp($routes, $cachedEndpoints, $latestEndpointsData, $groups);
+        $groupedEndpoints = Camel::groupEndpoints($endpoints, $this->endpointGroupIndexes);
+        $this->writeEndpointsToDisk($groupedEndpoints);
+        $this->writeExampleCustomEndpoint();
+        $groupedEndpoints = Camel::prepareGroupedEndpointsForOutput($groupedEndpoints);
+        return $groupedEndpoints;
+    }
+
+    /**
+     * @param MatchedRoute[] $matches
+     * @param array $cachedEndpoints
+     * @param array $latestEndpointsData
+     * @param array[] $groups
+     *
+     * @return array
+     * @throws \Exception
+     */
+    private function extractEndpointsInfoFromLaravelApp(array $matches, array $cachedEndpoints, array $latestEndpointsData, array $groups): array
+    {
+        $generator = new Extractor($this->docConfig);
+        $parsedEndpoints = [];
+
+        foreach ($matches as $routeItem) {
+            $route = $routeItem->getRoute();
+
+            $routeControllerAndMethod = u::getRouteClassAndMethodNames($route);
+            if (!$this->isValidRoute($routeControllerAndMethod)) {
+                c::warn('Skipping invalid route: ' . c::getRouteRepresentation($route));
+                continue;
+            }
+
+            if (!$this->doesControllerMethodExist($routeControllerAndMethod)) {
+                c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ' - Controller method does not exist.');
+                continue;
+            }
+
+            if ($this->isRouteHiddenFromDocumentation($routeControllerAndMethod)) {
+                c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ': @hideFromAPIDocumentation was specified.');
+                continue;
+            }
+
+            try {
+                c::info('Processing route: ' . c::getRouteRepresentation($route));
+                $currentEndpointData = $generator->processRoute($route, $routeItem->getRules());
+                // If latest data is different from cached data, merge latest into current
+                [$currentEndpointData, $index] = $this->mergeAnyEndpointDataUpdates($currentEndpointData, $cachedEndpoints, $latestEndpointsData, $groups);
+
+                // We need to preserve order of endpoints, in case user did custom sorting
+                $parsedEndpoints[] = $currentEndpointData;
+                if ($index !== null) {
+                    $this->endpointGroupIndexes[$currentEndpointData->endpointId()] = $index;
+                }
+                c::success('Processed route: ' . c::getRouteRepresentation($route));
+            } catch (\Exception $exception) {
+                $this->encounteredErrors = true;
+                c::error('Failed processing route: ' . c::getRouteRepresentation($route) . ' - Exception encountered.');
+                e::dumpExceptionIfVerbose($exception);
+            }
+        }
+
+        return $parsedEndpoints;
+    }
+
+    /**
+     * @param ExtractedEndpointData $endpointData
+     * @param array[] $cachedEndpoints
+     * @param array[] $latestEndpointsData
+     * @param array[] $groups
+     *
+     * @return array The extracted endpoint data and the endpoint's index in the group file
+     */
+    private function mergeAnyEndpointDataUpdates(ExtractedEndpointData $endpointData, array $cachedEndpoints, array $latestEndpointsData, array $groups): array
+    {
+        // First, find the corresponding endpoint in cached and latest
+        $thisEndpointCached = Arr::first($cachedEndpoints, function (array $endpoint) use ($endpointData) {
+            return $endpoint['uri'] === $endpointData->uri && $endpoint['httpMethods'] === $endpointData->httpMethods;
+        });
+        if (!$thisEndpointCached) {
+            return [$endpointData, null];
+        }
+
+        $thisEndpointLatest = Arr::first($latestEndpointsData, function (array $endpoint) use ($endpointData) {
+            return $endpoint['uri'] === $endpointData->uri && $endpoint['httpMethods'] == $endpointData->httpMethods;
+        });
+        if (!$thisEndpointLatest) {
+            return [$endpointData, null];
+        }
+
+        // Then compare cached and latest to see what sections changed.
+        $properties = [
+            'metadata',
+            'headers',
+            'urlParameters',
+            'queryParameters',
+            'bodyParameters',
+            'responses',
+            'responseFields',
+        ];
+
+        $changed = [];
+        foreach ($properties as $property) {
+            if ($thisEndpointCached[$property] != $thisEndpointLatest[$property]) {
+                $changed[] = $property;
+            }
+        }
+
+        // Finally, merge any changed sections.
+        $thisEndpointLatest = OutputEndpointData::create($thisEndpointLatest);
+        foreach ($changed as $property) {
+            $endpointData->$property = $thisEndpointLatest->$property;
+        }
+        $index = Camel::getEndpointIndexInGroup($groups, $thisEndpointLatest);
+
+        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)) {
+            [$classOrObject, $method] = $routeControllerAndMethod;
+            if (u::isInvokableObject($classOrObject)) {
+                return true;
+            }
+            $routeControllerAndMethod = $classOrObject . '@' . $method;
+        }
+
+        return !is_callable($routeControllerAndMethod) && !is_null($routeControllerAndMethod);
+    }
+
+    private function doesControllerMethodExist(array $routeControllerAndMethod): bool
+    {
+        [$class, $method] = $routeControllerAndMethod;
+        $reflection = new ReflectionClass($class);
+
+        if ($reflection->hasMethod($method)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private function isRouteHiddenFromDocumentation(array $routeControllerAndMethod): bool
+    {
+        if (!($class = $routeControllerAndMethod[0]) instanceof \Closure) {
+            $classDocBlock = new DocBlock((new ReflectionClass($class))->getDocComment() ?: '');
+            $shouldIgnoreClass = collect($classDocBlock->getTags())
+                ->filter(function (Tag $tag) {
+                    return Str::lower($tag->getName()) === 'hidefromapidocumentation';
+                })->isNotEmpty();
+
+            if ($shouldIgnoreClass) {
+                return true;
+            }
+        }
+
+        $methodDocBlock = new DocBlock(u::getReflectedRouteMethod($routeControllerAndMethod)->getDocComment() ?: '');
+        $shouldIgnoreMethod = collect($methodDocBlock->getTags())
+            ->filter(function (Tag $tag) {
+                return Str::lower($tag->getName()) === 'hidefromapidocumentation';
+            })->isNotEmpty();
+
+        return $shouldIgnoreMethod;
+    }
+
+    protected function writeExampleCustomEndpoint(): void
+    {
+        // We add an example to guide users in case they need to add a custom endpoint.
+        if (!file_exists(static::$camelDir . '/custom.0.yaml')) {
+            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();
+    }
+}

+ 24 - 0
src/GroupedEndpoints/GroupedEndpointsFromCamelDir.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace Knuckles\Scribe\GroupedEndpoints;
+
+use Knuckles\Camel\Camel;
+use Knuckles\Scribe\Commands\GenerateDocumentation;
+
+class GroupedEndpointsFromCamelDir implements GroupedEndpointsContract
+{
+    public function get(): array
+    {
+        if (!is_dir(GenerateDocumentation::$camelDir)) {
+            throw new \InvalidArgumentException("Can't use --no-extraction because there are no endpoints in the " .
+                GenerateDocumentation::$camelDir . " directory.");
+        }
+
+        return Camel::loadEndpointsIntoGroups(GenerateDocumentation::$camelDir);
+    }
+
+    public function hasEncounteredErrors(): bool
+    {
+        return false;
+    }
+}