Browse Source

Implement basic subgroup support

shalvah 2 years ago
parent
commit
7cf0773864

+ 6 - 0
CHANGELOG.md

@@ -12,6 +12,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 
 ### Removed
 
+# 4.0.0
+### Added
+- Support for specifying groups and endpoints order in config file ([29ddcfc](https://github.com/knuckleswtf/scribe/commit/29ddcfcf284a06da0ae6cb399d09ee5cf1f9ffa7)))
+- Support for specifying example model sources ([39ff208](https://github.com/knuckleswtf/scribe/commit/39ff208085d68eed4c459768ac5a1120934f021a)))
+- Support for subgroups ([39ff208](https://github.com/knuckleswtf/scribe/commit/39ff208085d68eed4c459768ac5a1120934f021a)))
+
 ## 3.33.0 (27 June 2022)
 ### Added
 - Include description in Postman collection for formdata body parameters ([10faa500](https://github.com/knuckleswtf/scribe/commit/10faa500e36e02d4efcecf8ad5e1d91ba1c7728d)))

+ 17 - 4
camel/Camel.php

@@ -165,10 +165,23 @@ class Camel
             if (empty($endpointGroupIndexes)) {
                 $groupName = data_get($endpointsInGroup[0], 'metadata.groupName');
                 if ($defaultGroupsOrder && isset($defaultGroupsOrder[$groupName])) {
-                    $endpointsOrder = Utils::getTopLevelItemsFromMixedConfigList($defaultGroupsOrder[$groupName]);
+                    $subGroupOrEndpointsOrder = Utils::getTopLevelItemsFromMixedConfigList($defaultGroupsOrder[$groupName]);
                     $sortedEndpoints = $endpointsInGroup->sortBy(
-                        function (ExtractedEndpointData $e) use ($endpointsOrder) {
-                            $index = array_search($e->httpMethods[0].' '.$e->uri, $endpointsOrder);
+                        function (ExtractedEndpointData $e) use ($defaultGroupsOrder, $subGroupOrEndpointsOrder) {
+                            $endpointIdentifier = $e->httpMethods[0].' /'.$e->uri;
+                            $index = array_search($e->metadata->subgroup, $subGroupOrEndpointsOrder);
+
+                            if ($index !== false) {
+                                // This is a subgroup
+                                $endpointsOrderInSubgroup = $defaultGroupsOrder[$e->metadata->groupName][$e->metadata->subgroup] ?? null;
+                                if ($endpointsOrderInSubgroup) {
+                                    $indexInSubGroup = array_search($endpointIdentifier, $endpointsOrderInSubgroup);
+                                    $index = ($indexInSubGroup === false) ? $index : ($index + ($indexInSubGroup * 0.1));
+                                }
+                            } else {
+                                // This is an endpoint
+                                $index = array_search($endpointIdentifier, $subGroupOrEndpointsOrder);
+                            }
                             return $index === false ? INF : $index;
                         },
                     );
@@ -238,4 +251,4 @@ class Camel
             return strnatcmp($a, $b);
         };
     }
-}
+}

+ 2 - 1
camel/Extraction/ExtractedEndpointData.php

@@ -196,11 +196,12 @@ class ExtractedEndpointData extends BaseDTO
     public function forSerialisation()
     {
         $copy = $this->except(
-        // Get rid of all duplicate data
+            // Get rid of all duplicate data
             'cleanQueryParameters', 'cleanUrlParameters', 'fileParameters', 'cleanBodyParameters',
             // and objects used only in extraction
             'route', 'controller', 'method', 'auth',
         );
+        // Remove these, since they're on the parent group object
         $copy->metadata = $copy->metadata->except('groupName', 'groupDescription', 'beforeGroup', 'afterGroup');
 
         return $copy;

+ 1 - 0
camel/Extraction/Metadata.php

@@ -7,6 +7,7 @@ use Knuckles\Camel\BaseDTO;
 class Metadata extends BaseDTO
 {
     public ?string $groupName;
+    public ?string $subgroup;
 
     /**
      * Name of the group that this group should be placed just before.

+ 12 - 6
config/scribe.php

@@ -314,17 +314,23 @@ INTRO
 
         /*
          * 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.
+         * You can customise that by listing the groups, subgroups 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.
-         * Note: omit the initial '/' when writing an endpoint.
+         * Any groups, subgroups or endpoints you don't list here will be added as usual after the ones here.
+         * If an endpoint/subgroups is listed under a group it doesn't belong in, it will be ignored.
+         * Note: you must include the initial '/' when writing an endpoint.
          */
         'order' => [
             // 'This group comes first',
             // 'This group comes next' => [
-            //    'POST this-endpoint-comes-first',
-            //    'GET this-endpoint-comes-next',
+            //     'POST /this-endpoint-comes-first',
+            //     'GET /this-endpoint-comes-next',
+            // ],
+            // 'This group comes third' => [
+            //     'This subgroup comes first' => [
+            //         'GET /this-other-endpoint-comes-first',
+            //         'GET /this-other-endpoint-comes-next',
+            //     ]
             // ]
         ],
     ],

+ 3 - 1
resources/example_custom_endpoint.yaml

@@ -1,6 +1,7 @@
 # To include an endpoint that isn't a part of your Laravel app (or belongs to a vendor package),
 # you can define it in a custom.*.yaml file, like this one.
 # Each custom file should contain an array of endpoints. Here's an example:
+# See https://scribe.knuckles.wtf/laravel/documenting/custom-endpoints#extra-sorting-groups-in-custom-endpoint-files for more options
 
 #- httpMethods:
 #    - POST
@@ -8,6 +9,7 @@
 #  metadata:
 #    groupName: The group the endpoint belongs to. Can be a new group or an existing group.
 #    groupDescription: A description for the group. You don't need to set this for every endpoint; once is enough.
+#    subgroup: You can add a subgroup, too.
 #    title: Do something
 #    description: 'This endpoint allows you to do something.'
 #    authenticated: false
@@ -48,4 +50,4 @@
 #    hey:
 #      name: hey
 #      description: Who knows?
-#      type: string # This is optional
+#      type: string # This is optional

+ 5 - 2
src/Commands/Upgrade.php

@@ -15,8 +15,11 @@ class Upgrade extends Command
     {
         $oldConfig = config('scribe');
         $upgrader = Upgrader::ofConfigFile('config/scribe.php', __DIR__ . '/../../config/scribe.php')
-            ->dontTouch('routes', 'laravel.middleware', 'postman.overrides', 'openapi.overrides')
-            ->move('interactive', 'try_it_out.enabled');
+            ->dontTouch('routes', 'laravel.middleware', 'postman.overrides', 'openapi.overrides',
+                'example_languages', 'database_connections_to_transact', 'strategies')
+            ->move('interactive', 'try_it_out.enabled')
+            ->move('default_group', 'groups.default')
+            ->move('faker_seed', 'examples.faker_seed');
 
         $changes = $upgrader->dryRun();
         if (empty($changes)) {

+ 47 - 34
src/Extracting/Strategies/Metadata/GetFromDocBlocks.php

@@ -20,11 +20,12 @@ class GetFromDocBlocks extends Strategy
 
     public function getMetadataFromDocBlock(DocBlock $methodDocBlock, DocBlock $classDocBlock): array
     {
-        [$routeGroupName, $routeGroupDescription, $routeTitle] = $this->getRouteGroupDescriptionAndTitle($methodDocBlock, $classDocBlock);
+        [$routeGroupName, $routeGroupDescription, $routeTitle] = $this->getEndpointGroupDetails($methodDocBlock, $classDocBlock);
 
         return [
             'groupName' => $routeGroupName,
             'groupDescription' => $routeGroupDescription,
+            'subgroup' => $this->getEndpointSubGroup($methodDocBlock, $classDocBlock),
             'title' => $routeTitle ?: $methodDocBlock->getShortDescription(),
             'description' => $methodDocBlock->getLongDescription()->getContents(),
             'authenticated' => $this->getAuthStatusFromDocBlock($methodDocBlock, $classDocBlock),
@@ -49,46 +50,41 @@ class GetFromDocBlocks extends Strategy
     }
 
     /**
-     * @param DocBlock $methodDocBlock
-     * @param DocBlock $controllerDocBlock
-     *
      * @return array The route group name, the group description, and the route title
      */
-    protected function getRouteGroupDescriptionAndTitle(DocBlock $methodDocBlock, DocBlock $controllerDocBlock)
+    protected function getEndpointGroupDetails(DocBlock $methodDocBlock, DocBlock $controllerDocBlock)
     {
-        // @group tag on the method overrides that on the controller
-        if (!empty($methodDocBlock->getTags())) {
-            foreach ($methodDocBlock->getTags() as $tag) {
-                if ($tag->getName() === 'group') {
-                    $routeGroupParts = explode("\n", trim($tag->getContent()));
-                    $routeGroupName = array_shift($routeGroupParts);
-                    $routeGroupDescription = trim(implode("\n", $routeGroupParts));
-
-                    // If the route has no title (the methodDocBlock's "short description"),
-                    // we'll assume the routeGroupDescription is actually the title
-                    // Something like this:
-                    // /**
-                    //   * Fetch cars. <-- This is route title.
-                    //   * @group Cars <-- This is group name.
-                    //   * APIs for cars. <-- This is group description (not required).
-                    //   **/
-                    // VS
-                    // /**
-                    //   * @group Cars <-- This is group name.
-                    //   * Fetch cars. <-- This is route title, NOT group description.
-                    //   **/
-
-                    // BTW, this is a spaghetti way of doing this.
-                    // It shall be refactored soon. Deus vult!💪
-                    if (empty($methodDocBlock->getShortDescription())) {
-                        return [$routeGroupName, '', $routeGroupDescription];
-                    }
-
-                    return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()];
+        foreach ($methodDocBlock->getTags() as $tag) {
+            if ($tag->getName() === 'group') {
+                $routeGroupParts = explode("\n", trim($tag->getContent()));
+                $routeGroupName = array_shift($routeGroupParts);
+                $routeGroupDescription = trim(implode("\n", $routeGroupParts));
+
+                // If the route has no title (the methodDocBlock's "short description"),
+                // we'll assume the routeGroupDescription is actually the title
+                // Something like this:
+                // /**
+                //   * Fetch cars. <-- This is route title.
+                //   * @group Cars <-- This is group name.
+                //   * APIs for cars. <-- This is group description (not required).
+                //   **/
+                // VS
+                // /**
+                //   * @group Cars <-- This is group name.
+                //   * Fetch cars. <-- This is route title, NOT group description.
+                //   **/
+
+                // BTW, this is a spaghetti way of doing this.
+                // It shall be refactored soon. Deus vult!💪
+                if (empty($methodDocBlock->getShortDescription())) {
+                    return [$routeGroupName, '', $routeGroupDescription];
                 }
+
+                return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()];
             }
         }
 
+        // Fall back to the controller
         foreach ($controllerDocBlock->getTags() as $tag) {
             if ($tag->getName() === 'group') {
                 $routeGroupParts = explode("\n", trim($tag->getContent()));
@@ -101,4 +97,21 @@ class GetFromDocBlocks extends Strategy
 
         return [$this->config->get('groups.default'), '', $methodDocBlock->getShortDescription()];
     }
+
+    protected function getEndpointSubGroup(DocBlock $methodDocBlock, DocBlock $controllerDocBlock): ?string
+    {
+        foreach ($methodDocBlock->getTags() as $tag) {
+            if ($tag->getName() === 'subgroup') {
+                return trim($tag->getContent());
+            }
+        }
+
+        foreach ($controllerDocBlock->getTags() as $tag) {
+            if ($tag->getName() === 'subgroup') {
+                return trim($tag->getContent());
+            }
+        }
+
+        return null;
+    }
 }

+ 39 - 0
tests/Fixtures/TestGroupController.php

@@ -40,4 +40,43 @@ class TestGroupController
     public function action10()
     {
     }
+
+    /**
+     * @group 13. Group 13
+     * @subgroup SG B
+     */
+    public function action13a()
+    {
+    }
+
+    /**
+     * @group 13. Group 13
+     * @subgroup SG C
+     */
+    public function action13b()
+    {
+    }
+
+    /**
+     * @group 13. Group 13
+     */
+    public function action13c()
+    {
+    }
+
+    /**
+     * @group 13. Group 13
+     * @subgroup SG B
+     */
+    public function action13d()
+    {
+    }
+
+    /**
+     * @group 13. Group 13
+     * @subgroup SG A
+     */
+    public function action13e()
+    {
+    }
 }

+ 36 - 10
tests/GenerateDocumentation/OutputTest.php

@@ -191,31 +191,57 @@ class OutputTest extends BaseLaravelTest
     /** @test */
     public function sorts_groups_and_endpoints_in_the_specified_order()
     {
-        $order = [
+        config(['scribe.groups.order' => [
             '10. Group 10',
             '1. Group 1' => [
-                'GET api/action1b',
-                'GET api/action1',
+                'GET /api/action1b',
+                'GET /api/action1',
             ],
-        ];
-        config(['scribe.groups.order' => $order]);
+            '13. Group 13' => [
+                'SG B' => [
+                    'POST /api/action13d',
+                    'GET /api/action13a',
+                ],
+                'SG A',
+                'PUT /api/action13c',
+                'POST /api/action13b',
+            ],
+        ]]);
 
-        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');
+        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']);
+        RouteFacade::get('/api/action13a', [TestGroupController::class, 'action13a']);
+        RouteFacade::post('/api/action13b', [TestGroupController::class, 'action13b']);
+        RouteFacade::put('/api/action13c', [TestGroupController::class, 'action13c']);
+        RouteFacade::post('/api/action13d', [TestGroupController::class, 'action13d']);
+        RouteFacade::get('/api/action13e', [TestGroupController::class, 'action13e']);
 
         $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']);
+        $thirdGroup = Yaml::parseFile('.scribe/endpoints/02.yaml');
+        $this->assertEquals('13. Group 13', $thirdGroup['name']);
+        $this->assertEquals('2. Group 2', Yaml::parseFile('.scribe/endpoints/03.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]);
+
+        $this->assertEquals('api/action13d', $thirdGroup['endpoints'][0]['uri']);
+        $this->assertEquals('POST', $thirdGroup['endpoints'][0]['httpMethods'][0]);
+        $this->assertEquals('api/action13a', $thirdGroup['endpoints'][1]['uri']);
+        $this->assertEquals('GET', $thirdGroup['endpoints'][1]['httpMethods'][0]);
+        $this->assertEquals('api/action13e', $thirdGroup['endpoints'][2]['uri']);
+        $this->assertEquals('GET', $thirdGroup['endpoints'][2]['httpMethods'][0]);
+        $this->assertEquals('api/action13c', $thirdGroup['endpoints'][3]['uri']);
+        $this->assertEquals('PUT', $thirdGroup['endpoints'][3]['httpMethods'][0]);
+        $this->assertEquals('api/action13b', $thirdGroup['endpoints'][4]['uri']);
+        $this->assertEquals('POST', $thirdGroup['endpoints'][4]['httpMethods'][0]);
     }
 
     /** @test */

+ 23 - 0
tests/Strategies/Metadata/GetFromDocBlocksTest.php

@@ -12,6 +12,26 @@ class GetFromDocBlocksTest extends TestCase
 {
     use ArraySubsetAsserts;
 
+    /** @test */
+    public function can_fetch_metadata_from_method_docblock()
+    {
+        $strategy = new GetFromDocBlocks(new DocumentationConfig([]));
+        $methodDocblock = <<<DOCBLOCK
+/**
+  * Endpoint title.
+  * Endpoint description.
+  * Multiline.
+  */
+DOCBLOCK;
+        $classDocblock = '';
+        $results = $strategy->getMetadataFromDocBlock(new DocBlock($methodDocblock), new DocBlock($classDocblock));
+
+        $this->assertFalse($results['authenticated']);
+        $this->assertNull($results['subgroup']);
+        $this->assertSame('Endpoint title.', $results['title']);
+        $this->assertSame("Endpoint description.\nMultiline.", $results['description']);
+    }
+
     /** @test */
     public function can_fetch_metadata_from_method_and_class()
     {
@@ -32,6 +52,7 @@ DOCBLOCK;
         $results = $strategy->getMetadataFromDocBlock(new DocBlock($methodDocblock), new DocBlock($classDocblock));
 
         $this->assertFalse($results['authenticated']);
+        $this->assertNull($results['subgroup']);
         $this->assertSame('Group A', $results['groupName']);
         $this->assertSame('Group description.', $results['groupDescription']);
         $this->assertSame('Endpoint title.', $results['title']);
@@ -46,12 +67,14 @@ DOCBLOCK;
         $classDocblock = <<<DOCBLOCK
 /**
   * @authenticated
+  * @subgroup Scheiße
   */
 DOCBLOCK;
         $results = $strategy->getMetadataFromDocBlock(new DocBlock($methodDocblock), new DocBlock($classDocblock));
 
         $this->assertTrue($results['authenticated']);
         $this->assertSame(null, $results['groupName']);
+        $this->assertSame('Scheiße', $results['subgroup']);
         $this->assertSame('', $results['groupDescription']);
         $this->assertSame('Endpoint title.', $results['title']);
         $this->assertSame("", $results['description']);