Explorar o código

Implement ordering of groups and endpoints in config file (closes #452)

shalvah %!s(int64=2) %!d(string=hai) anos
pai
achega
29ddcfcf28

+ 54 - 6
camel/Camel.php

@@ -144,19 +144,36 @@ class Camel
     /**
      * @param array[] $endpoints
      * @param array $endpointGroupIndexes Mapping of endpoint IDs to their index within their group
+     * @param array $defaultGroupsOrder The order for groups that users specified in their config file.
      *
      * @return array[]
      */
-    public static function groupEndpoints(array $endpoints, array $endpointGroupIndexes): array
+    public static function groupEndpoints(array $endpoints, array $endpointGroupIndexes, array $defaultGroupsOrder = []): array
     {
-        $groupedEndpoints = collect($endpoints)
-            ->groupBy('metadata.groupName')
-            ->sortKeys(SORT_NATURAL);
+        $groupedEndpoints = collect($endpoints)->groupBy('metadata.groupName');
 
-        return $groupedEndpoints->map(function (Collection $endpointsInGroup) use ($endpointGroupIndexes) {
+        if ($defaultGroupsOrder) {
+            $groupsOrder = Utils::getTopLevelItemsFromMixedConfigList($defaultGroupsOrder);
+            $groupedEndpoints = $groupedEndpoints->sortKeysUsing(self::getOrderListComparator($groupsOrder));
+        } else {
+            $groupedEndpoints = $groupedEndpoints->sortKeys(SORT_NATURAL);
+        }
+
+        return $groupedEndpoints->map(function (Collection $endpointsInGroup) use ($defaultGroupsOrder, $endpointGroupIndexes) {
             /** @var Collection<(int|string),ExtractedEndpointData> $endpointsInGroup */
             $sortedEndpoints = $endpointsInGroup;
-            if (!empty($endpointGroupIndexes)) {
+            if (empty($endpointGroupIndexes)) {
+                $groupName = data_get($endpointsInGroup[0], 'metadata.groupName');
+                if ($defaultGroupsOrder && isset($defaultGroupsOrder[$groupName])) {
+                    $endpointsOrder = Utils::getTopLevelItemsFromMixedConfigList($defaultGroupsOrder[$groupName]);
+                    $sortedEndpoints = $endpointsInGroup->sortBy(
+                        function (ExtractedEndpointData $e) use ($endpointsOrder) {
+                            $index = array_search($e->httpMethods[0].' '.$e->uri, $endpointsOrder);
+                            return $index === false ? INF : $index;
+                        },
+                    );
+                }
+            } else {
                 $sortedEndpoints = $endpointsInGroup->sortBy(
                     fn(ExtractedEndpointData $e) => $endpointGroupIndexes[$e->endpointId()] ?? INF,
                 );
@@ -190,4 +207,35 @@ class Camel
         }, $groupedEndpoints);
         return array_values(Arr::sort($groups, 'fileName'));
     }
+
+    /**
+     * Given an $order list like ['first', 'second', ...], return a compare function that can be used to sort
+     * a list of strings based on the $order list. Any strings not in the list are sorted with natural sort.
+     *
+     * @param array $order
+     */
+    public static function getOrderListComparator(array $order): \Closure
+    {
+        return function ($a, $b) use ($order) {
+            $indexOfA = array_search($a, $order);
+            $indexOfB = array_search($b, $order);
+
+            if ($indexOfA !== false && $indexOfB !== false) {
+                return $indexOfA <=> $indexOfB;
+            }
+
+            // If only the first is in the default order, then it must come before the second.
+            if ($indexOfA !== false) {
+                return -1;
+            }
+
+            // If only the second is in the default order, then first must come after it.
+            if ($indexOfB !== false) {
+                return 1;
+            }
+
+            // If neither is present, fall back to natural sort
+            return strnatcmp($a, $b);
+        };
+    }
 }

+ 21 - 4
config/scribe.php

@@ -306,10 +306,27 @@ INTRO
         ],
     ],
 
-    /*
-     * Endpoints which don't have a @group will be placed in this default group.
-     */
-    'default_group' => 'Endpoints',
+    'groups' => [
+        /*
+         * Endpoints which don't have a @group will be placed in this default group.
+         */
+        'default' => 'Endpoints',
+
+        /*
+         * By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined.
+         * You can customise that by listing the groups and endpoints here in the order you want them.
+         *
+         * Any groups or endpoints you don't list here will be added as usual after the ones here.
+         * If an endpoint is listed under a group it doesn't belong in, it will be ignored.
+         */
+        'order' => [
+            // 'Group 1',
+            // 'Group 2' => [
+            //    'POST /this-endpoint-comes-first',
+            //    'GET /this-endpoint-comes-next',
+            // ]
+        ],
+    ],
 
     /*
      * Custom logo path. This will be used as the value of the src attribute for the <img> tag,

+ 1 - 1
src/GroupedEndpoints/GroupedEndpointsFromApp.php

@@ -78,7 +78,7 @@ class GroupedEndpointsFromApp implements GroupedEndpointsContract
 
         $routes = $routeMatcher->getRoutes($this->docConfig->get('routes'), $this->docConfig->get('router'));
         $endpoints = $this->extractEndpointsInfoFromLaravelApp($routes, $cachedEndpoints, $latestEndpointsData, $groups);
-        $groupedEndpoints = Camel::groupEndpoints($endpoints, $this->endpointGroupIndexes);
+        $groupedEndpoints = Camel::groupEndpoints($endpoints, $this->endpointGroupIndexes, $this->docConfig->get('groups.order', []));
         $this->writeEndpointsToDisk($groupedEndpoints);
         $groupedEndpoints = Camel::prepareGroupedEndpointsForOutput($groupedEndpoints);
         return $groupedEndpoints;

+ 0 - 24
src/Scribe.php

@@ -5,7 +5,6 @@ namespace Knuckles\Scribe;
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Knuckles\Scribe\Tools\Globals;
 use Symfony\Component\HttpFoundation\Request;
-use Illuminate\Support\Collection;
 
 class Scribe
 {
@@ -57,27 +56,4 @@ class Scribe
     {
         Globals::$__instantiateFormRequestUsing = $callable;
     }
-
-    /**
-     * Customise how Scribe orders your endpoint groups.
-     * Your callback will be given
-     *
-     * @param callable(string,\Illuminate\Routing\Route,\ReflectionFunctionAbstract): mixed $callable
-     */
-    public static function orderGroupsUsing(callable $callable)
-    {
-        Globals::$__orderGroupsUsing = $callable;
-    }
-
-    /**
-     * Customise how Scribe orders endpoints within a group.
-     * Your callback will be given a Laravel Collection of ExtractedEndpointData objects,
-     * and should return the sorted collection.
-     *
-     * @param callable(Collection<ExtractedEndpointData>): Collection<ExtractedEndpointData> $callable
-     */
-    public static function orderEndpointsInGroupUsing(callable $callable)
-    {
-        Globals::$__orderEndpointsInGroupUsing = $callable;
-    }
 }

+ 0 - 4
src/Tools/Globals.php

@@ -17,8 +17,4 @@ class Globals
     public static $__afterGenerating;
 
     public static $__instantiateFormRequestUsing;
-
-    public static $__orderEndpointsInGroupUsing;
-
-    public static $__orderGroupsUsing;
 }

+ 25 - 0
src/Tools/Utils.php

@@ -22,6 +22,31 @@ use Throwable;
 
 class Utils
 {
+    /**
+     * Sometimes you have a config array that can have items as keys or values, like this:
+     *
+     * [
+     *   'a',
+     *   'b' => [ (options for b) ]
+     * ]
+     *
+     * This method extracts the top-level options (['a', 'b'])
+     *
+     * @param array $mixedList
+     */
+    public static function getTopLevelItemsFromMixedConfigList(array $mixedList): array
+    {
+        $topLevels = [];
+        foreach ($mixedList as $item => $value) {
+            if (is_int($item)) {
+                $topLevels[] = $value;
+            } else {
+                $topLevels[] = $item;
+            }
+        }
+        return $topLevels;
+    }
+
     public static function getUrlWithBoundParameters(string $uri, array $urlParameters = []): string
     {
         return self::replaceUrlParameterPlaceholdersWithValues($uri, $urlParameters);

+ 30 - 0
tests/GenerateDocumentation/OutputTest.php

@@ -198,6 +198,36 @@ class OutputTest extends BaseLaravelTest
         $this->assertEquals('10. Group 10', Yaml::parseFile('.scribe/endpoints/02.yaml')['name']);
     }
 
+    /** @test */
+    public function sorts_groups_and_endpoints_in_the_specified_order()
+    {
+        $order = [
+            '10. Group 10',
+            '1. Group 1' => [
+                'GET api/action1b',
+                'GET api/action1',
+            ],
+        ];
+        config(['scribe.groups.order' => $order]);
+
+        RouteFacade::get('/api/action1', TestGroupController::class . '@action1');
+        RouteFacade::get('/api/action1b', TestGroupController::class . '@action1b');
+        RouteFacade::get('/api/action2', TestGroupController::class . '@action2');
+        RouteFacade::get('/api/action10', TestGroupController::class . '@action10');
+
+        $this->generate();
+
+        $this->assertEquals('10. Group 10', Yaml::parseFile('.scribe/endpoints/00.yaml')['name']);
+        $secondGroup = Yaml::parseFile('.scribe/endpoints/01.yaml');
+        $this->assertEquals('1. Group 1', $secondGroup['name']);
+        $this->assertEquals('2. Group 2', Yaml::parseFile('.scribe/endpoints/02.yaml')['name']);
+
+        $this->assertEquals('api/action1b', $secondGroup['endpoints'][0]['uri']);
+        $this->assertEquals('GET', $secondGroup['endpoints'][0]['httpMethods'][0]);
+        $this->assertEquals('api/action1', $secondGroup['endpoints'][1]['uri']);
+        $this->assertEquals('GET', $secondGroup['endpoints'][1]['httpMethods'][0]);
+    }
+
     /** @test */
     public function will_not_overwrite_manually_modified_content_unless_force_flag_is_set()
     {