Browse Source

Implement beforeGroup and afterGroup for sorting custom endpoints' groups

shalvah 3 years ago
parent
commit
f28c9771a0

+ 1 - 1
camel/Extraction/ExtractedEndpointData.php

@@ -188,7 +188,7 @@ class ExtractedEndpointData extends BaseDTO
             // and objects used only in extraction
             'route', 'controller', 'method', 'auth',
         );
-        $copy->metadata = $copy->metadata->except('groupName', 'groupDescription');
+        $copy->metadata = $copy->metadata->except('groupName', 'groupDescription', 'beforeGroup', 'afterGroup');
 
         return $copy;
     }

+ 17 - 10
camel/Extraction/Metadata.php

@@ -7,18 +7,25 @@ use Knuckles\Camel\BaseDTO;
 
 class Metadata extends BaseDTO
 {
-    /** @var string|null */
-    public $groupName;
+    public ?string $groupName;
 
-    /** @var string|null */
-    public $groupDescription;
+    /**
+     * Name of the group that this group should be placed just before.
+     * Only used in custom endpoints, if the endpoint's `groupName` doesn't already exist.
+     */
+    public ?string $beforeGroup;
 
-    /** @var string|null */
-    public $title;
+    /**
+     * Name of the group that this group should be placed just after.
+     * Only used in custom endpoints, if the endpoint's `groupName` doesn't already exist.
+     */
+    public ?string $afterGroup;
 
-    /** @var string|null */
-    public $description;
+    public ?string $groupDescription;
 
-    /** @var bool */
-    public $authenticated = false;
+    public ?string $title;
+
+    public ?string $description;
+
+    public bool $authenticated = false;
 }

+ 44 - 4
src/Commands/GenerateDocumentation.php

@@ -7,6 +7,7 @@ use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\URL;
 use Knuckles\Camel\Camel;
 use Knuckles\Camel\Output\OutputEndpointData;
+use Knuckles\Scribe\Exceptions\GroupNotFound;
 use Knuckles\Scribe\GroupedEndpoints\GroupedEndpointsFactory;
 use Knuckles\Scribe\Matching\RouteMatcherInterface;
 use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
@@ -106,19 +107,58 @@ class GenerateDocumentation extends Command
     protected function mergeUserDefinedEndpoints(array $groupedEndpoints, array $userDefinedEndpoints): array
     {
         foreach ($userDefinedEndpoints as $endpoint) {
-            $existingGroupKey = Arr::first(array_keys($groupedEndpoints), function ($key) use ($groupedEndpoints, $endpoint) {
+            $indexOfGroupWhereThisEndpointShouldBeAdded = Arr::first(array_keys($groupedEndpoints), function ($key) use ($groupedEndpoints, $endpoint) {
                 $group = $groupedEndpoints[$key];
                 return $group['name'] === ($endpoint['metadata']['groupName'] ?? $this->docConfig->get('default_group', ''));
             });
 
-            if ($existingGroupKey !== null) {
-                $groupedEndpoints[$existingGroupKey]['endpoints'][] = OutputEndpointData::fromExtractedEndpointArray($endpoint);
+            if ($indexOfGroupWhereThisEndpointShouldBeAdded !== null) {
+                $groupedEndpoints[$indexOfGroupWhereThisEndpointShouldBeAdded]['endpoints'][] = OutputEndpointData::fromExtractedEndpointArray($endpoint);
             } else {
-                $groupedEndpoints[] = [
+                $newGroup = [
                     'name' => $endpoint['metadata']['groupName'] ?? $this->docConfig->get('default_group', ''),
                     'description' => $endpoint['metadata']['groupDescription'] ?? null,
                     'endpoints' => [OutputEndpointData::fromExtractedEndpointArray($endpoint)],
                 ];
+
+                // Place the new group directly before/after an existing group
+                // if `beforeGroup` or `afterGroup` was set.
+                $beforeGroupName = $endpoint['metadata']['beforeGroup'] ?? null;
+                $afterGroupName = $endpoint['metadata']['afterGroup'] ?? null;
+
+                if ($beforeGroupName) {
+                    $found = false;
+                    $sortedGroupedEndpoints = [];
+                    foreach ($groupedEndpoints as $group) {
+                        if ($group['name'] === $beforeGroupName) {
+                            $found = true;
+                            $sortedGroupedEndpoints[] = $newGroup;
+                        }
+                        $sortedGroupedEndpoints[] = $group;
+                    }
+
+                    if (!$found) {
+                        throw GroupNotFound::forTag($beforeGroupName, "beforeGroup:");
+                    }
+                    $groupedEndpoints = $sortedGroupedEndpoints;
+                } else if ($afterGroupName) {
+                    $found = false;
+                    $sortedGroupedEndpoints = [];
+                    foreach ($groupedEndpoints as $group) {
+                        $sortedGroupedEndpoints[] = $group;
+                        if ($group['name'] === $afterGroupName) {
+                            $found = true;
+                            $sortedGroupedEndpoints[] = $newGroup;
+                        }
+                    }
+
+                    if (!$found) {
+                        throw GroupNotFound::forTag($afterGroupName, "afterGroup:");
+                    }
+                    $groupedEndpoints = $sortedGroupedEndpoints;
+                } else {
+                    $groupedEndpoints[] = $newGroup;
+                }
             }
         }
 

+ 17 - 0
src/Exceptions/GroupNotFound.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Knuckles\Scribe\Exceptions;
+
+class GroupNotFound extends \RuntimeException implements ScribeException
+{
+    public static function forTag(string $groupName, string $tag)
+    {
+        return new self(
+            <<<MESSAGE
+You specified the group "$groupName" in a "$tag" field in one of your custom endpoints, but we couldn't find that group.
+Did you rename the group?
+MESSAGE
+
+        );
+    }
+}

+ 4 - 0
src/Exceptions/ScribeException.php

@@ -2,6 +2,10 @@
 
 namespace Knuckles\Scribe\Exceptions;
 
+/**
+ * Scribe Exceptions are thrown intentionally by us, and should not be swallowed.
+ * They are meant to crash the task and be thrown back to the user.
+ */
 interface ScribeException
 {
 }

+ 4 - 0
src/Extracting/ParsesValidationRules.php

@@ -66,6 +66,8 @@ trait ParsesValidationRules
                 $parameters[$parameter] = $parameterData;
             } catch (Throwable $e) {
                 if ($e instanceof ScribeException) {
+                    // This is a lower-level error that we've encountered and wrapped;
+                    // Pass it on to the user.
                     throw $e;
                 }
                 throw ProblemParsingValidationRules::forParam($parameter, $e);
@@ -101,6 +103,8 @@ trait ParsesValidationRules
                 $parameters[$parameter] = $parameterData;
             } catch (Throwable $e) {
                 if ($e instanceof ScribeException) {
+                    // This is a lower-level error that we've encountered and wrapped;
+                    // Pass it on to the user.
                     throw $e;
                 }
                 throw ProblemParsingValidationRules::forParam($parameter, $e);

+ 47 - 1
tests/Fixtures/custom.0.yaml

@@ -35,4 +35,50 @@
     hey:
       name: hey
       description: Who knows?
-      type: string
+      type: string
+
+- httpMethods:
+    - GET
+  uri: withBeforeGroup
+  metadata:
+    groupName: '5. Group 5'
+    beforeGroup: '2. Group 2'
+    title:
+    description:
+    authenticated: false
+  headers: {}
+  urlParameters: {}
+  queryParameters: {}
+  bodyParameters: {}
+  responses: {}
+  responseFields: {}
+- httpMethods:
+    - GET
+  uri: withAfterGroup
+  metadata:
+    groupName: '4. Group 4'
+    afterGroup: '5. Group 5'
+    title:
+    description:
+    authenticated: false
+  headers: {}
+  urlParameters: {}
+  queryParameters: {}
+  bodyParameters: {}
+  responses: {}
+  responseFields: {}
+
+- httpMethods:
+    - GET
+  uri: belongingToAnEarlierBeforeGroup
+  metadata:
+    groupName: '5. Group 5'
+    title:
+    description:
+    authenticated: false
+  headers: {}
+  urlParameters: {}
+  queryParameters: {}
+  bodyParameters: {}
+  responses: {}
+  responseFields: {}

+ 12 - 7
tests/GenerateDocumentationTest.php

@@ -406,7 +406,7 @@ class GenerateDocumentationTest extends BaseLaravelTest
     }
 
     /** @test */
-    public function merges_user_defined_endpoints()
+    public function merges_and_correctly_sorts_user_defined_endpoints()
     {
         RouteFacade::get('/api/action1', [TestGroupController::class, 'action1']);
         RouteFacade::get('/api/action2', [TestGroupController::class, 'action2']);
@@ -420,18 +420,23 @@ class GenerateDocumentationTest extends BaseLaravelTest
 
         $crawler = new Crawler(file_get_contents('public/docs/index.html'));
         $headings = $crawler->filter('h1')->getIterator();
-        // There should only be four headings — intro, auth and two groups
-        $this->assertCount(4, $headings);
-        [$_, $_, $group1, $group2] = $headings;
+        // There should only be six headings — intro, auth and four groups
+        $this->assertCount(6, $headings);
+        [$_, $_, $group1, $group2, $group3, $group4] = $headings;
         $this->assertEquals('1. Group 1', trim($group1->textContent));
-        $this->assertEquals('2. Group 2', trim($group2->textContent));
+        $this->assertEquals('5. Group 5', trim($group2->textContent));
+        $this->assertEquals('4. Group 4', trim($group3->textContent));
+        $this->assertEquals('2. Group 2', trim($group4->textContent));
         $expectedEndpoints = $crawler->filter('h2');
-        $this->assertEquals(3, $expectedEndpoints->count());
+        $this->assertEquals(6, $expectedEndpoints->count());
         // Enforce the order of the endpoints
         // Ideally, we should also check the groups they're under
         $this->assertEquals("Some endpoint.", $expectedEndpoints->getNode(0)->textContent);
         $this->assertEquals("User defined", $expectedEndpoints->getNode(1)->textContent);
-        $this->assertEquals("GET api/action2", $expectedEndpoints->getNode(2)->textContent);
+        $this->assertEquals("GET withBeforeGroup", $expectedEndpoints->getNode(2)->textContent);
+        $this->assertEquals("GET belongingToAnEarlierBeforeGroup", $expectedEndpoints->getNode(3)->textContent);
+        $this->assertEquals("GET withAfterGroup", $expectedEndpoints->getNode(4)->textContent);
+        $this->assertEquals("GET api/action2", $expectedEndpoints->getNode(5)->textContent);
     }
 
     /** @test */