Browse Source

First Camel impl - serialise to YAML

shalvah 4 years ago
parent
commit
f7c3320463

+ 90 - 0
camel/Camel.php

@@ -0,0 +1,90 @@
+<?php
+
+
+namespace Knuckles\Camel;
+
+use Illuminate\Support\Arr;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Str;
+use Knuckles\Camel\Output\EndpointData;
+use League\Flysystem\Adapter\Local;
+use League\Flysystem\Filesystem;
+use Symfony\Component\Yaml\Yaml;
+
+
+class Camel
+{
+    /**
+     * Load endpoints from the Camel files into groups (arrays).
+     *
+     * @param string $folder
+     * @return array[]
+     */
+    public static function loadEndpointsIntoGroups(string $folder): array
+    {
+        $groups = [];
+        self::loadEndpointsFromCamelFiles($folder, function ($group) use ($groups) {
+            $group['endpoints'] = array_map(function (array $endpoint) {
+                return new EndpointData($endpoint);
+            }, $group['endpoints']);
+            $groups[] = $group;
+        });
+        return $groups;
+    }
+
+    /**
+     * Load endpoints from the Camel files into a flat list of endpoint arrays.
+     *
+     * @param string $folder
+     * @return array[]
+     */
+    public static function loadEndpointsToFlatPrimitivesArray(string $folder): array
+    {
+        $endpoints = [];
+        self::loadEndpointsFromCamelFiles($folder, function ($group) use ($endpoints) {
+            foreach ($group['endpoints'] as $endpoint) {
+                $endpoints[] = $endpoint;
+            }
+        });
+        return $endpoints;
+    }
+
+    public static function loadEndpointsFromCamelFiles(string $folder, callable $callback)
+    {
+        $adapter = new Local(getcwd());
+        $fs = new Filesystem($adapter);
+        $contents = $fs->listContents($folder);;
+
+        foreach ($contents as $object) {
+            if ($object['type'] == 'file' && Str::endsWith($object['basename'], '.yaml')) {
+                $group = Yaml::parseFile($object['path']);
+                $callback($group);
+            }
+        }
+    }
+
+    public static function doesGroupContainEndpoint(array $group, EndpointData $endpoint): bool
+    {
+        return boolval(Arr::first($group['endpoints'], fn(EndpointData $e) => $e->endpointId() === $endpoint->endpointId()));
+    }
+
+    /**
+     * @param array[] $endpoints
+     *
+     * @return array[]
+     */
+    public static function groupEndpoints(array $endpoints): array
+    {
+        $groupedEndpoints = collect($endpoints)
+            ->groupBy('metadata.groupName')
+            ->sortKeys(SORT_NATURAL);
+
+        return $groupedEndpoints->map(fn(Collection $group) => [
+            'name' => $group[0]->metadata->groupName,
+            'description' => Arr::first($group, function ($endpointData) {
+                    return $endpointData->metadata->groupDescription !== '';
+                })->metadata->groupDescription ?? '',
+            'endpoints' => $group->map(fn($endpointData) => $endpointData->forOutput())->all(),
+        ])->all();
+    }
+}

+ 4 - 8
camel/Extraction/EndpointData.php

@@ -45,9 +45,6 @@ class EndpointData extends BaseDTO
      */
     public array $cleanQueryParameters = [];
 
-    /**
-     * @var array<string, \Knuckles\Camel\Extraction\Parameter>
-     */
     public array $bodyParameters = [];
 
     /**
@@ -136,16 +133,15 @@ class EndpointData extends BaseDTO
 
     /**
      * Prepare the endpoint data for serialising.
-     * @return array
      */
-    public function forOutput(): array
+    public function forOutput(): \Knuckles\Camel\Output\EndpointData
     {
         $this->metadata = $this->metadata->except('groupName', 'groupDescription');
-        return $this->except(
-        // Get rid of all duplicate data
+        return new \Knuckles\Camel\Output\EndpointData($this->except(
+            // Get rid of all duplicate data
             'cleanQueryParameters', 'cleanUrlParameters', 'fileParameters', 'cleanBodyParameters',
             // and objects only needed for extraction
             'route', 'controller', 'method',
-        )->toArray();
+        )->toArray());
     }
 }

+ 2 - 0
camel/Output/EndpointData.php

@@ -9,6 +9,7 @@ use Knuckles\Camel\Extraction\ResponseCollection;
 use Knuckles\Camel\Extraction\ResponseField;
 use Knuckles\Scribe\Extracting\Extractor;
 use Knuckles\Scribe\Tools\Utils as u;
+use Knuckles\Camel\Extraction\Metadata;
 
 
 class EndpointData extends BaseDTO
@@ -58,6 +59,7 @@ class EndpointData extends BaseDTO
     public array $cleanBodyParameters = [];
 
     /**
+     * @var array
      * @var array<string,\Illuminate\Http\UploadedFile>
      */
     public array $fileParameters = [];

+ 0 - 31
camel/Output/Group.php

@@ -1,31 +0,0 @@
-<?php
-
-namespace Knuckles\Camel\Output;
-
-
-use Illuminate\Support\Arr;
-use Knuckles\Camel\BaseDTO;
-
-class Group extends BaseDTO
-{
-    public string $name;
-    public ?string $description;
-
-    /**
-     * @var \Knuckles\Camel\Output\EndpointData[] $endpoints
-     */
-    public array $endpoints = [];
-
-    public static function createFromSpec(array $spec): Group
-    {
-        $spec['endpoints'] = array_map(
-            fn($endpoint) => new EndpointData($endpoint), $spec['endpoints']
-        );
-        return new Group($spec);
-    }
-
-    public function has(EndpointData $endpoint)
-    {
-        return boolval(Arr::first($this->endpoints, fn(EndpointData $e) => $e->endpointId() === $endpoint->endpointId()));
-    }
-}

+ 0 - 19
camel/Output/Metadata.php

@@ -1,19 +0,0 @@
-<?php
-
-namespace Knuckles\Camel\Output;
-
-
-use Knuckles\Camel\BaseDTO;
-
-class Metadata extends BaseDTO
-{
-    public ?string $groupName;
-
-    public ?string $groupDescription;
-
-    public ?string $title;
-
-    public ?string $description;
-
-    public bool $authenticated = false;
-}

+ 1 - 1
camel/Output/Parameter.php

@@ -5,7 +5,7 @@ namespace Knuckles\Camel\Output;
 
 use Knuckles\Camel\BaseDTO;
 
-class Parameter extends BaseDTO
+class Parameter extends \Knuckles\Camel\Extraction\Parameter
 {
     public string $name;
     public ?string $description = null;

+ 0 - 33
camel/Tools/Loader.php

@@ -1,33 +0,0 @@
-<?php
-
-
-namespace Knuckles\Camel\Tools;
-
-use Illuminate\Support\Str;
-use Knuckles\Camel\Output\Group;
-use League\Flysystem\Adapter\Local;
-use League\Flysystem\Filesystem;
-use Symfony\Component\Yaml\Yaml;
-
-
-class Loader
-{
-    /**
-     * @param string $folder
-     * @return Group[]
-     */
-    public static function loadEndpoints(string $folder): array
-    {
-        $adapter = new Local(getcwd());
-        $fs = new Filesystem($adapter);
-        $contents = $fs->listContents($folder);;
-
-        $groups = [];
-        foreach ($contents as $object) {
-            if ($object['type'] == 'file' && Str::endsWith($object['basename'], '.yaml')) {
-                $groups[] = Group::createFromSpec(Yaml::parseFile($object['path']));
-            }
-        }
-        return $groups;
-    }
-}

+ 0 - 34
camel/Tools/Serialiser.php

@@ -1,34 +0,0 @@
-<?php
-
-
-namespace Knuckles\Camel\Tools;
-
-use Illuminate\Support\Arr;
-use Illuminate\Support\Collection;
-use Knuckles\Camel\Extraction\EndpointData;
-
-
-class Serialiser
-{
-    /**
-     * @param EndpointData[] $endpoints
-     */
-    public static function serialiseEndpointsForOutput(array $endpoints): array
-    {
-        $groupedEndpoints = collect($endpoints)
-            ->groupBy('metadata.groupName')
-            ->sortBy(
-                static fn(Collection $group) => $group->first()->metadata->groupName,
-                SORT_NATURAL
-            );
-
-        return $groupedEndpoints->map(fn(Collection $group) => [
-            'name' => $group[0]->metadata->groupName,
-            'description' => Arr::first($group, function (EndpointData $endpointData) {
-                    return $endpointData->metadata->groupDescription !== '';
-                })->metadata->groupDescription ?? '',
-            'endpoints' => $group->map(fn(EndpointData $endpointData) => $endpointData->forOutput())
-                ->toArray(),
-        ])->toArray();
-    }
-}

+ 126 - 57
src/Commands/GenerateDocumentation.php

@@ -3,12 +3,14 @@
 namespace Knuckles\Scribe\Commands;
 
 use Illuminate\Console\Command;
-use Illuminate\Routing\Route;
-use Illuminate\Support\Collection;
+use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\URL;
 use Illuminate\Support\Str;
-use Knuckles\Camel\Tools\Loader;
-use Knuckles\Camel\Tools\Serialiser;
+use Knuckles\Camel\Extraction\EndpointData as ExtractedEndpointData;
+use Knuckles\Camel\Output\EndpointData;
+use Knuckles\Camel\Output\EndpointData as OutputEndpointData;
+use Knuckles\Camel\Output\Group;
+use Knuckles\Camel\Camel;
 use Knuckles\Scribe\Extracting\Extractor;
 use Knuckles\Scribe\Matching\MatchedRoute;
 use Knuckles\Scribe\Matching\RouteMatcherInterface;
@@ -22,26 +24,15 @@ use Knuckles\Scribe\Writing\Writer;
 use Mpociot\Reflection\DocBlock;
 use Mpociot\Reflection\DocBlock\Tag;
 use ReflectionClass;
-use ReflectionException;
 use Symfony\Component\Yaml\Yaml;
 
 class GenerateDocumentation extends Command
 {
-    /**
-     * The name and signature of the console command.
-     *
-     * @var string
-     */
     protected $signature = "scribe:generate
                             {--force : Discard any changes you've made to the Markdown files}
                             {--no-extraction : Skip extraction of route info and just transform the Markdown files}
     ";
-
-    /**
-     * The console command description.
-     *
-     * @var string
-     */
+    
     protected $description = 'Generate API documentation from your Laravel/Dingo routes.';
 
     /**
@@ -49,64 +40,60 @@ class GenerateDocumentation extends Command
      */
     private $docConfig;
 
+    public static $camelDir = ".scribe/endpoints";
+    public static $cacheDir = ".scribe/endpoints.cache";
+
     /**
-     * @var string
+     * @var bool
      */
-    private $baseUrl;
+    private $shouldExtract;
 
     /**
-     * Execute the console command.
-     *
-     * @param RouteMatcherInterface $routeMatcher
-     *
-     * @return void
+     * @var bool
      */
-    public function handle(RouteMatcherInterface $routeMatcher)
+    private $forcing;
+
+    public function handle(RouteMatcherInterface $routeMatcher): void
     {
         $this->bootstrap();
 
-        $noExtraction = $this->option('no-extraction');
-        $camelDir = ".endpoints";
-
-        if (!$noExtraction) {
+        if ($this->forcing) {
             $routes = $routeMatcher->getRoutes($this->docConfig->get('routes'), $this->docConfig->get('router'));
             $endpoints = $this->extractEndpointsInfo($routes);
-            $serialised = Serialiser::serialiseEndpointsForOutput($endpoints);
-
-            // Utils::deleteDirectoryAndContents($comparisonDir);
-
-            if (!is_dir($camelDir)) {
-                mkdir($camelDir);
+            $groupedEndpoints = Camel::groupEndpoints($endpoints);
+            $this->writeEndpointsToDisk($groupedEndpoints);
+        } else if ($this->shouldExtract) {
+            $latestEndpointsData = [];
+            $cachedEndpoints = [];
+
+            if (is_dir(static::$camelDir) && is_dir(static::$cacheDir)) {
+                $latestEndpointsData = Camel::loadEndpointsToFlatPrimitivesArray(static::$camelDir);
+                $cachedEndpoints = Camel::loadEndpointsToFlatPrimitivesArray(static::$cacheDir);
             }
 
-            $i = 0;
-            foreach ($serialised as $groupName => $endpointsInGroup) {
-                file_put_contents(
-                    "$camelDir/$i.yaml",
-                    Yaml::dump(
-                        $endpointsInGroup,
-                        10,
-                        2,
-                        Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK
-                    )
-                );
-                $i++;
+            $routes = $routeMatcher->getRoutes($this->docConfig->get('routes'), $this->docConfig->get('router'));
+            $endpoints = $this->extractEndpointsInfo($routes, $cachedEndpoints, $latestEndpointsData);
+            $groupedEndpoints = Camel::groupEndpoints($endpoints);
+            $this->writeEndpointsToDisk($groupedEndpoints);
+        } else {
+            if (!is_dir(static::$camelDir)) {
+                throw new \Exception("Can't use --no-extraction because there are no endpoints in the {static::$camelDir} directory.");
             }
+            $groupedEndpoints = Camel::loadEndpointsIntoGroups(static::$camelDir);
         }
 
-        $endpoints = Loader::loadEndpoints($camelDir);
-
-        $writer = new Writer($this->docConfig, $this->option('force'));
-        $writer->writeDocs($endpoints);
+        $writer = new Writer($this->docConfig, $this->forcing);
+        $writer->writeDocs($groupedEndpoints);
     }
 
     /**
      * @param MatchedRoute[] $matches
+     * @param array $cachedEndpoints
+     * @param array $latestEndpointsData
      *
      * @return array
-     *
      */
-    private function extractEndpointsInfo(array $matches): array
+    private function extractEndpointsInfo(array $matches, array $cachedEndpoints = [], array $latestEndpointsData = []): array
     {
         $generator = new Extractor($this->docConfig);
         $parsedRoutes = [];
@@ -131,7 +118,10 @@ class GenerateDocumentation extends Command
 
             try {
                 c::info('Processing route: ' . c::getRouteRepresentation($route));
-                $parsedRoutes[] = $generator->processRoute($route, $routeItem->getRules());
+                $currentEndpointData = $generator->processRoute($route, $routeItem->getRules());
+                // If latest data is different from cached data, merge latest into current
+                $currentEndpointData = $this->mergeAnyEndpointDataUpdates($currentEndpointData, $cachedEndpoints, $latestEndpointsData);
+                $parsedRoutes[] = $currentEndpointData;
                 c::success('Processed route: ' . c::getRouteRepresentation($route));
             } catch (\Exception $exception) {
                 c::error('Failed processing route: ' . c::getRouteRepresentation($route) . ' - Exception encountered.');
@@ -142,6 +132,51 @@ class GenerateDocumentation extends Command
         return $parsedRoutes;
     }
 
+    private function mergeAnyEndpointDataUpdates(ExtractedEndpointData $endpointData, array $cachedEndpoints, array $latestEndpointsData): ExtractedEndpointData
+    {
+        // First, find the corresponding endpoint in cached and latest
+        $thisEndpointCached = Arr::first($cachedEndpoints, function ($endpoint) use ($endpointData) {
+            return $endpoint['uri'] === $endpointData->uri && $endpoint['methods'] === $endpointData->methods;
+        });
+        if (!$thisEndpointCached) {
+            return $endpointData;
+        }
+
+        $thisEndpointLatest = Arr::first($latestEndpointsData, function ($endpoint) use ($endpointData) {
+            return $endpoint['uri'] === $endpointData->uri && $endpoint['methods'] == $endpointData->methods;
+        });
+        if (!$thisEndpointLatest) {
+            return $endpointData;
+        }
+
+        // Then compare cached and latest to see what sections changed.
+        $properties = [
+            'metadata',
+            'headers',
+            'urlParameters',
+            'queryParameters',
+            'bodyParameters',
+            'responses',
+            'responseFields',
+            'auth',
+        ];
+
+        $changed = [];
+        foreach ($properties as $property) {
+            if ($thisEndpointCached[$property] != $thisEndpointLatest[$property]) {
+                $changed[] = $property;
+            }
+        }
+
+        // Finally, merge any changed sections.
+        foreach ($changed as $property) {
+            $thisEndpointLatest = OutputEndpointData::create($thisEndpointLatest);
+            $endpointData->$property = $thisEndpointLatest->$property;
+        }
+
+        return $endpointData;
+    }
+
     private function isValidRoute(array $routeControllerAndMethod = null): bool
     {
         if (is_array($routeControllerAndMethod)) {
@@ -192,16 +227,50 @@ class GenerateDocumentation extends Command
 
     public function bootstrap(): void
     {
-        // Using a global static variable here, so 🙄 if you don't like it.
-        // Also, the --verbose option is included with all Artisan commands.
+        // The --verbose option is included with all Artisan commands.
         Globals::$shouldBeVerbose = $this->option('verbose');
 
         c::bootstrapOutput($this->output);
 
         $this->docConfig = new DocumentationConfig(config('scribe'));
-        $this->baseUrl = $this->docConfig->get('base_url') ?? config('app.url');
 
         // Force root URL so it works in Postman collection
-        URL::forceRootUrl($this->baseUrl);
+        $baseUrl = $this->docConfig->get('base_url') ?? config('app.url');
+        URL::forceRootUrl($baseUrl);
+
+        $this->forcing = $this->option('force');
+        $this->shouldExtract = !$this->option('no-extraction');
+
+        if ($this->forcing && !$this->shouldExtract) {
+            throw new \Exception("Can't use --force and --no-extraction together.");
+        }
+    }
+
+    protected function writeEndpointsToDisk(array $grouped): void
+    {
+        Utils::deleteDirectoryAndContents(static::$camelDir);
+        Utils::deleteDirectoryAndContents(static::$cacheDir);
+
+        if (!is_dir(static::$camelDir)) {
+            mkdir(static::$camelDir, 0777, true);
+        }
+
+        if (!is_dir(static::$cacheDir)) {
+            mkdir(static::$cacheDir, 0777, true);
+        }
+
+        $i = 0;
+        foreach ($grouped as $group) {
+            $group['endpoints'] = array_map(function (EndpointData $endpoint) {
+                return $endpoint->toArray();
+            }, $group['endpoints']);
+            $yaml = Yaml::dump(
+                $group, 10, 2,
+                Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK
+            );
+            file_put_contents(static::$camelDir."/$i.yaml", $yaml);
+            file_put_contents(static::$cacheDir."/$i.yaml", "## Autogenerated by Scribe. DO NOT MODIFY.\n\n" . $yaml);
+            $i++;
+        }
     }
 }

+ 4 - 3
src/Writing/OpenAPISpecWriter.php

@@ -5,6 +5,7 @@ namespace Knuckles\Scribe\Writing;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Str;
+use Knuckles\Camel\Camel;
 use Knuckles\Camel\Output\EndpointData;
 use Knuckles\Camel\Output\Group;
 use Knuckles\Camel\Output\Parameter;
@@ -40,7 +41,7 @@ class OpenAPISpecWriter
     /**
      * See https://swagger.io/specification/
      *
-     * @param Group[] $groupedEndpoints
+     * @param array[] $groupedEndpoints
      *
      * @return array
      */
@@ -63,7 +64,7 @@ class OpenAPISpecWriter
     }
 
     /**
-     * @param Group[] $groupedEndpoints
+     * @param array[] $groupedEndpoints
      *
      * @return mixed
      */
@@ -82,7 +83,7 @@ class OpenAPISpecWriter
                     'description' => $endpoint->metadata->description,
                     'parameters' => $this->generateEndpointParametersSpec($endpoint),
                     'responses' => $this->generateEndpointResponsesSpec($endpoint),
-                    'tags' => [Arr::first($groupedEndpoints, fn(Group $group) => $group->has($endpoint))->name],
+                    'tags' => [Arr::first($groupedEndpoints, fn($group) => Camel::doesGroupContainEndpoint($group, $endpoint))['name']],
                 ];
 
                 if (count($endpoint->bodyParameters)) {

+ 5 - 6
src/Writing/PostmanCollectionWriter.php

@@ -4,7 +4,6 @@ namespace Knuckles\Scribe\Writing;
 
 use Illuminate\Support\Str;
 use Knuckles\Camel\Output\EndpointData;
-use Knuckles\Camel\Output\Group;
 use Knuckles\Camel\Output\Parameter;
 use Knuckles\Scribe\Tools\DocumentationConfig;
 use Ramsey\Uuid\Uuid;
@@ -30,7 +29,7 @@ class PostmanCollectionWriter
     }
 
     /**
-     * @param Group[] $groupedEndpoints
+     * @param array[] $groupedEndpoints
      *
      * @return array
      */
@@ -52,11 +51,11 @@ class PostmanCollectionWriter
                 'description' => $this->config->get('description', ''),
                 'schema' => "https://schema.getpostman.com/json/collection/v" . self::VERSION . "/collection.json",
             ],
-            'item' => array_map(function (Group $group) {
+            'item' => array_map(function (array $group) {
                 return [
-                    'name' => $group->name,
-                    'description' => $group->description,
-                    'item' => array_map(\Closure::fromCallable([$this, 'generateEndpointItem']), $group->endpoints),
+                    'name' => $group['name'],
+                    'description' => $group['description'],
+                    'item' => array_map(\Closure::fromCallable([$this, 'generateEndpointItem']), $group['endpoints']),
                 ];
             }, $groupedEndpoints),
             'auth' => $this->generateAuthObject(),

+ 14 - 16
src/Writing/Writer.php

@@ -3,11 +3,9 @@
 namespace Knuckles\Scribe\Writing;
 
 use Illuminate\Support\Arr;
-use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Str;
 use Knuckles\Camel\Output\EndpointData;
-use Knuckles\Camel\Output\Group;
 use Knuckles\Pastel\Pastel;
 use Knuckles\Scribe\Tools\ConsoleOutputUtils;
 use Knuckles\Scribe\Tools\DocumentationConfig;
@@ -95,7 +93,7 @@ class Writer
     }
 
     /**
-     * @param Group[] $groups
+     * @param array[] $groups
      */
     public function writeDocs(array $groups)
     {
@@ -117,7 +115,7 @@ class Writer
     }
 
     /**
-     * @param Group[] $groups
+     * @param array[] $groups
      *
      * @return void
      */
@@ -149,7 +147,7 @@ class Writer
     }
 
     /**
-     * @param Group[] $groups
+     * @param array[] $groups
      * @param array $settings
      *
      * @return array
@@ -157,8 +155,8 @@ class Writer
      */
     public function generateMarkdownOutputForEachRoute(array $groups, array $settings): array
     {
-        $routesWithOutput = array_map(function (Group $group) use ($settings) {
-            $group->endpoints = array_map(function (EndpointData $endpointData) use ($settings) {
+        $routesWithOutput = array_map(function ($group) use ($settings) {
+            $group['endpoints'] = array_map(function (EndpointData $endpointData) use ($settings) {
                 $hasRequestOptions = !empty($endpointData->headers)
                     || !empty($endpointData->cleanQueryParameters)
                     || !empty($endpointData->cleanBodyParameters);
@@ -182,7 +180,7 @@ class Writer
                     ->render();
 
                 return $endpointData;
-            }, $group->endpoints);
+            }, $group['endpoints']);
             return $group;
         }, $groups);
 
@@ -228,7 +226,7 @@ class Writer
     /**
      * Generate Postman collection JSON file.
      *
-     * @param Group[] $groupedEndpoints
+     * @param array[] $groupedEndpoints
      *
      * @return string
      */
@@ -251,7 +249,7 @@ class Writer
     }
 
     /**
-     * @param Group[] $groupedEndpoints
+     * @param array[] $groupedEndpoints
      *
      * @return string
      */
@@ -404,7 +402,7 @@ class Writer
     }
 
     /**
-     * @param Group[] $groups
+     * @param array[] $groups
      * @param array $settings
      */
     protected function writeEndpointsMarkdownFile(array $groups, array $settings): void
@@ -415,8 +413,8 @@ class Writer
 
         // Generate Markdown for each route. Not using a Blade component bc of some complex logic
         $groupsWithOutput = $this->generateMarkdownOutputForEachRoute($groups, $settings);
-        $groupFileNames = array_map(function (Group $group) {
-            $groupId = Str::slug($group->name);
+        $groupFileNames = array_map(function ($group) {
+            $groupId = Str::slug($group['name']);
             $routeGroupMarkdownFile = $this->sourceOutputPath . "/groups/$groupId.md";
 
             if ($this->hasFileBeenModified($routeGroupMarkdownFile)) {
@@ -429,9 +427,9 @@ class Writer
             }
 
             $groupMarkdown = view('scribe::partials.group')
-                ->with('groupName', $group->name)
-                ->with('groupDescription', $group->description)
-                ->with('routes', $group->endpoints);
+                ->with('groupName', $group['name'])
+                ->with('groupDescription', $group['description'])
+                ->with('routes', $group['endpoints']);
 
             $this->writeFile($routeGroupMarkdownFile, $groupMarkdown);
             return "$groupId.md";