Browse Source

Basic support for overriding docs for inherited methods

shalvah 2 years ago
parent
commit
9735fdf150
3 changed files with 157 additions and 4 deletions
  1. 26 1
      camel/BaseDTO.php
  2. 52 3
      src/Extracting/Extractor.php
  3. 79 0
      tests/Unit/ExtractorTest.php

+ 26 - 1
camel/BaseDTO.php

@@ -6,7 +6,7 @@ use Illuminate\Contracts\Support\Arrayable;
 use Spatie\DataTransferObject\DataTransferObject;
 
 
-class BaseDTO extends DataTransferObject implements Arrayable
+class BaseDTO extends DataTransferObject implements Arrayable, \ArrayAccess
 {
     /**
      * @var array $custom
@@ -48,4 +48,29 @@ class BaseDTO extends DataTransferObject implements Arrayable
 
         return $array;
     }
+
+    public static function make(array|self $data): static
+    {
+        return $data instanceof static ? $data : new static($data);
+    }
+
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->$offset);
+    }
+
+    public function offsetGet(mixed $offset): mixed
+    {
+        return $this->$offset;
+    }
+
+    public function offsetSet(mixed $offset, mixed $value): void
+    {
+        $this->$offset = $value;
+    }
+
+    public function offsetUnset(mixed $offset): void
+    {
+        unset($this->$offset);
+    }
 }

+ 52 - 3
src/Extracting/Extractor.php

@@ -11,6 +11,7 @@ use Illuminate\Http\UploadedFile;
 use Illuminate\Routing\Route;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Str;
+use Knuckles\Camel\Extraction\ResponseCollection;
 use Knuckles\Camel\Extraction\ResponseField;
 use Knuckles\Camel\Output\OutputEndpointData;
 use Knuckles\Scribe\Extracting\Strategies\Strategy;
@@ -43,7 +44,6 @@ class Extractor
      * @param array $routeRules Rules to apply when generating documentation for this route
      *
      * @return ExtractedEndpointData
-     * @throws \ReflectionException
      *
      */
     public function processRoute(Route $route, array $routeRules = []): ExtractedEndpointData
@@ -51,20 +51,32 @@ class Extractor
         self::$routeBeingProcessed = $route;
 
         $endpointData = ExtractedEndpointData::fromRoute($route);
+
+        $inheritedDocsOverrides = [];
+        if ($endpointData?->controller->hasMethod('inheritedDocsOverrides')) {
+            $inheritedDocsOverrides = call_user_func([$endpointData->controller->getName(), 'inheritedDocsOverrides']);
+            $inheritedDocsOverrides = $inheritedDocsOverrides[$endpointData->method->getName()] ?? [];
+        }
+
         $this->fetchMetadata($endpointData, $routeRules);
+        $this->mergeInheritedMethodsData('metadata', $endpointData, $inheritedDocsOverrides);
 
         $this->fetchUrlParameters($endpointData, $routeRules);
+        $this->mergeInheritedMethodsData('urlParameters', $endpointData, $inheritedDocsOverrides);
         $endpointData->cleanUrlParameters = self::cleanParams($endpointData->urlParameters);
 
         $this->addAuthField($endpointData);
 
         $this->fetchQueryParameters($endpointData, $routeRules);
+        $this->mergeInheritedMethodsData('queryParameters', $endpointData, $inheritedDocsOverrides);
         $endpointData->cleanQueryParameters = self::cleanParams($endpointData->queryParameters);
 
         $this->fetchRequestHeaders($endpointData, $routeRules);
+        $this->mergeInheritedMethodsData('headers', $endpointData, $inheritedDocsOverrides);
 
         $this->fetchBodyParameters($endpointData, $routeRules);
         $endpointData->cleanBodyParameters = self::cleanParams($endpointData->bodyParameters);
+        $this->mergeInheritedMethodsData('bodyParameters', $endpointData, $inheritedDocsOverrides);
 
         if (count($endpointData->cleanBodyParameters) && !isset($endpointData->headers['Content-Type'])) {
             // Set content type if the user forgot to set it
@@ -81,8 +93,10 @@ class Extractor
         $endpointData->cleanBodyParameters = $regularParameters;
 
         $this->fetchResponses($endpointData, $routeRules);
+        $this->mergeInheritedMethodsData('responses', $endpointData, $inheritedDocsOverrides);
 
         $this->fetchResponseFields($endpointData, $routeRules);
+        $this->mergeInheritedMethodsData('responseFields', $endpointData, $inheritedDocsOverrides);
 
         self::$routeBeingProcessed = null;
 
@@ -93,7 +107,7 @@ class Extractor
     {
         $endpointData->metadata = new Metadata([
             'groupName' => $this->config->get('groups.default', ''),
-            "authenticated" => $this->config->get("auth.default", false)
+            "authenticated" => $this->config->get("auth.default", false),
         ]);
 
         $this->iterateThroughStrategies('metadata', $endpointData, $rulesToApply, function ($results) use ($endpointData) {
@@ -444,7 +458,7 @@ class Extractor
                 // When the body is an array, param names will be  "[].paramname",
                 // so $parts is ['[]']
                 if ($parts[0] == '[]') {
-                    $dotPathToParent = '[]'.$dotPathToParent;
+                    $dotPathToParent = '[]' . $dotPathToParent;
                 }
 
                 $dotPath = $dotPathToParent . '.__fields.' . $fieldName;
@@ -471,4 +485,39 @@ class Extractor
 
         return $finalParameters;
     }
+
+    protected function mergeInheritedMethodsData(string $stage, ExtractedEndpointData $endpointData, array $inheritedDocsOverrides = []): void
+    {
+        $overrides = $inheritedDocsOverrides[$stage] ?? [];
+        $normalizeparamData = fn($data, $key) => array_merge($data, ["name" => $key]);
+        if (is_array($overrides)) {
+            foreach ($overrides as $key => $item) {
+                switch ($stage) {
+                    case "responses":
+                        $endpointData->responses->concat($overrides);
+                        $endpointData->responses->sortBy('status');
+                        break;
+                    case "urlParameters":
+                    case "bodyParameters":
+                    case "queryParameters":
+                        $endpointData->$stage[$key] = Parameter::make($normalizeparamData($item, $key));
+                        break;
+                    case "responseFields":
+                        $endpointData->$stage[$key] = ResponseField::make($normalizeparamData($item, $key));
+                        break;
+                    default:
+                        $endpointData->$stage[$key] = $item;
+                }
+            }
+        } else if (is_callable($overrides)) {
+            $results = $overrides($endpointData);
+
+            $endpointData->$stage = match ($stage) {
+                "responses" => ResponseCollection::make($results),
+                "urlParameters", "bodyParameters", "queryParameters" => collect($results)->map(fn($param, $name) => Parameter::make($normalizeparamData($param, $name)))->all(),
+                "responseFields" => collect($results)->map(fn($field, $name) => ResponseField::make($normalizeparamData($field, $name)))->all(),
+                default => $results,
+            };
+        }
+    }
 }

+ 79 - 0
tests/Unit/ExtractorTest.php

@@ -4,6 +4,7 @@ namespace Knuckles\Scribe\Tests\Unit;
 
 use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
 use Illuminate\Routing\Route;
+use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Knuckles\Camel\Extraction\Parameter;
 use Knuckles\Scribe\Attributes\UrlParam;
 use Knuckles\Scribe\Extracting\Extractor;
@@ -266,6 +267,32 @@ class ExtractorTest extends TestCase
         $this->assertSame('some custom metadata', $parsed->metadata->custom['myProperty']);
     }
 
+    /** @test */
+    public function can_override_data_for_inherited_methods()
+    {
+        $route = $this->createRoute('POST', '/api/test', 'endpoint', TestParentController::class);
+        $parent = $this->extractor->processRoute($route);
+        $this->assertSame('Parent title', $parent->metadata->title);
+        $this->assertSame('Parent group name', $parent->metadata->groupName);
+        $this->assertSame('Parent description', $parent->metadata->description);
+        $this->assertCount(1, $parent->responses);
+        $this->assertCount(1, $parent->bodyParameters);
+        $this->assertArraySubset(["type" => "integer"], $parent->bodyParameters['thing']->toArray());
+        $this->assertEmpty($parent->queryParameters);
+
+        $inheritedRoute = $this->createRoute('POST', '/api/test', 'endpoint', TestInheritedController::class);
+        $inherited = $this->extractor->processRoute($inheritedRoute);
+        $this->assertSame('Overridden title', $inherited->metadata->title);
+        $this->assertSame('Overridden group name', $inherited->metadata->groupName);
+        $this->assertSame('Parent description', $inherited->metadata->description);
+        $this->assertCount(0, $inherited->responses);
+        $this->assertCount(2, $inherited->bodyParameters);
+        $this->assertArraySubset(["type" => "integer"], $inherited->bodyParameters['thing']->toArray());
+        $this->assertArraySubset(["type" => "string"], $inherited->bodyParameters["other_thing"]->toArray());
+        $this->assertCount(1, $inherited->queryParameters);
+        $this->assertArraySubset(["type" => "string"], $inherited->queryParameters["queryThing"]->toArray());
+    }
+
     public function createRoute(string $httpMethod, string $path, string $controllerMethod, $class = TestController::class)
     {
         return new Route([$httpMethod], $path, ['uses' => [$class, $controllerMethod]]);
@@ -352,3 +379,55 @@ class ExtractorTest extends TestCase
         ];
     }
 }
+
+
+class TestParentController
+{
+    /**
+     * Parent title
+     *
+     * Parent description
+     *
+     * @group Parent group name
+     *
+     * @bodyParam thing integer
+     * @response {"hello":"there"}
+     */
+    public function endpoint()
+    {
+
+    }
+}
+
+class TestInheritedController extends TestParentController
+{
+    public static function inheritedDocsOverrides()
+    {
+        return [
+            "endpoint" => [
+                "metadata" => [
+                    "title" => "Overridden title",
+                    "groupName" => "Overridden group name",
+                ],
+                "queryParameters" => function (ExtractedEndpointData $endpointData) {
+                    // Overrides
+                    return [
+                        'queryThing' => [
+                            'type' => 'string',
+                        ],
+                    ];
+                },
+                "bodyParameters" => [
+                    // Merges
+                    "other_thing" => [
+                        "type" => "string",
+                    ],
+                ],
+                "responses" => function (ExtractedEndpointData $endpointData) {
+                    // Completely overrides responses
+                    return [];
+                },
+            ],
+        ];
+    }
+}