Browse Source

Add support for states, relations, and pagination in API Resources and Transformer

shalvah 5 years ago
parent
commit
688020662c

+ 1 - 1
docs/plugins.md

@@ -65,7 +65,7 @@ The last thing to do is to register the strategy. Strategies are registered in a
             \Knuckles\Scribe\Extracting\Strategies\QueryParameters\GetFromQueryParamTag::class,
         ],
         'headers' => [
-            \Knuckles\Scribe\Extracting\Strategies\RequestHeaders\GetFromRouteRules::class,
+            \Knuckles\Scribe\Extracting\Strategies\Headers\GetFromRouteRules::class,
         ],
         'bodyParameters' => [
             \Knuckles\Scribe\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class,

+ 47 - 0
docs/whats-new.md

@@ -0,0 +1,47 @@
+## Documenting responses
+You can now give readers more information about the fields they can expect in your responses. This functionality is provided by default by the `UseResponseFieldTags` strategy. You use it by adding a `@responseField` annotation to your controller method.
+
+```
+@responseField id integer The id of the newly created user
+```
+
+Note that this also works the same way for array responses. So if your response is an array of objects, you should only mention the keys of the objects inside the array. So the above annotation will work fine for both this response:
+
+```
+{
+  "id": 3
+}
+```
+
+and this:
+
+```
+[
+  { "id": 3 }
+]
+```
+
+You can also omit the type of the field. Scribe will try to figure it out from the 2xx responses for that endpoint. So this gives the same result:
+
+```
+@responseField id integer The id of the newly created user
+```
+
+Result:
+
+![](./images/response-fields.png)
+
+
+## Automatic routing for `laravel` docs
+The `autoload` key in `laravel` config is now `add_routes`, and is `true` by default. This means you don't have to do any extra steps to serve your docs through you Laravel app.
+
+## Authentication
+Scribe can now add authentication information to your docs! To get this, you'll need to use the `auth` section in the config file.
+
+The info you provide will be used in generating a description of the authentication text, as well as adding the needed parameters in the example requests, and in response calls. See that section of the docs for details.
+
+## More customization options
+You can now customise the introductory text by setting the `intro_text` key in your scribe.php. 
+
+## Reworked Strategy API
+- `stage` property.

+ 0 - 18
src/Extracting/Strategies/Responses/ResponseCalls.php

@@ -89,7 +89,6 @@ class ResponseCalls extends Strategy
     private function configureEnvironment(array $rulesToApply)
     {
         $this->startDbTransaction();
-        $this->setEnvironmentVariables($rulesToApply['env'] ?? []);
         $this->setLaravelConfigs($rulesToApply['config'] ?? []);
     }
 
@@ -125,23 +124,6 @@ class ResponseCalls extends Strategy
         return $request;
     }
 
-    /**
-     * @param array $env
-     *
-     * @return void
-     *
-     * @deprecated Not guaranteed to overwrite application's env. Use Laravel config variables instead.
-     */
-    private function setEnvironmentVariables(array $env)
-    {
-        foreach ($env as $name => $value) {
-            putenv("$name=$value");
-
-            $_ENV[$name] = $value;
-            $_SERVER[$name] = $value;
-        }
-    }
-
     /**
      * @param array $config
      *

+ 27 - 6
src/Extracting/Strategies/Responses/UseApiResourceTags.php

@@ -8,6 +8,8 @@ use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 use Illuminate\Http\Resources\Json\ResourceCollection;
 use Illuminate\Http\Response;
+use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Pagination\Paginator;
 use Illuminate\Routing\Route;
 use Illuminate\Support\Arr;
 use Knuckles\Scribe\Tools\AnnotationParser;
@@ -72,7 +74,7 @@ class UseApiResourceTags extends Strategy
         }
 
         list($statusCode, $apiResourceClass) = $this->getStatusCodeAndApiResourceClass($apiResourceTag);
-        [$model, $factoryStates, $relations] = $this->getClassToBeTransformed($tags);
+        [$model, $factoryStates, $relations, $pagination] = $this->getClassToBeTransformedAndAttributes($tags);
         $modelInstance = $this->instantiateApiResourceModel($model, $factoryStates, $relations);
 
         try {
@@ -87,9 +89,25 @@ class UseApiResourceTags extends Strategy
             // or a ResourceCollection (via `new`)
             // See https://laravel.com/docs/5.8/eloquent-resources
             $models = [$modelInstance, $this->instantiateApiResourceModel($model, $factoryStates, $relations)];
+            if (count($pagination) == 1) {
+                $perPage = $pagination[0];
+                $paginator = new LengthAwarePaginator(
+                    // For some reason, the LengthAware paginator needs only first page items to work correctly
+                    collect($models)->slice(0, $perPage),
+                    count($models),
+                    $perPage
+                );
+                $list = $paginator;
+            } else if (count($pagination) == 2 && $pagination[0] == 'simple') {
+                $perPage = $pagination[1];
+                $paginator = new Paginator($models, $perPage);
+                $list = $paginator;
+            } else {
+                $list = collect($models);
+            }
             $resource = $resource instanceof ResourceCollection
-                ? new $apiResourceClass(collect($models))
-                : $apiResourceClass::collection(collect($models));
+                ? new $apiResourceClass($list)
+                : $apiResourceClass::collection($list);
         }
 
         /** @var Response $response */
@@ -118,7 +136,7 @@ class UseApiResourceTags extends Strategy
         return [$status, $apiResourceClass];
     }
 
-    private function getClassToBeTransformed(array $tags): array
+    private function getClassToBeTransformedAndAttributes(array $tags): array
     {
         $modelTag = Arr::first(array_filter($tags, function ($tag) {
             return ($tag instanceof Tag) && strtolower($tag->getName()) == 'apiresourcemodel';
@@ -126,17 +144,20 @@ class UseApiResourceTags extends Strategy
 
         $type = null;
         $states = [];
+        $relations = [];
+        $pagination = [];
         if ($modelTag) {
-            ['content' => $type, 'attributes' => $attributes] = AnnotationParser::parseIntoContentAndAttributes($modelTag->getContent(), ['states', 'with']);
+            ['content' => $type, 'attributes' => $attributes] = AnnotationParser::parseIntoContentAndAttributes($modelTag->getContent(), ['states', 'with', 'paginate']);
             $states = $attributes['states'] ? explode(',', $attributes['states']) : [];
             $relations = $attributes['with'] ? explode(',', $attributes['with']) : [];
+            $pagination = $attributes['paginate'] ? explode(',', $attributes['paginate']) : [];
         }
 
         if (empty($type)) {
             throw new Exception("Couldn't detect an Eloquent API resource model from your docblock. Did you remember to specify a model using @apiResourceModel?");
         }
 
-        return [$type, $states, $relations];
+        return [$type, $states, $relations, $pagination];
     }
 
     /**

+ 44 - 6
src/Extracting/Strategies/Responses/UseTransformerTags.php

@@ -5,6 +5,7 @@ namespace Knuckles\Scribe\Extracting\Strategies\Responses;
 use Exception;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Model as IlluminateModel;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Routing\Route;
 use Illuminate\Support\Arr;
 use Knuckles\Scribe\Tools\AnnotationParser;
@@ -80,12 +81,22 @@ class UseTransformerTags extends Strategy
                 $fractal->setSerializer(app($this->config->get('fractal.serializer')));
             }
 
-            $resource = (strtolower($transformerTag->getName()) == 'transformercollection')
-                ? new Collection(
-                    [$modelInstance, $this->instantiateTransformerModel($model, $factoryStates, $relations)],
-                    new $transformer()
-                )
-                : new Item($modelInstance, new $transformer());
+        if ((strtolower($transformerTag->getName()) == 'transformercollection')) {
+            $models = [$modelInstance, $this->instantiateTransformerModel($model, $factoryStates, $relations)];
+            $resource = new Collection($models, new $transformer());
+
+            ['adapter' => $paginatorAdapter, 'perPage' => $perPage] = $this->getTransformerPaginatorData($tags);
+            if ($paginatorAdapter) {
+                $total = count($models);
+                // Need to pass only the first page to both adapter and paginator, otherwise they will display ebverything
+                $firstPage = collect($models)->slice(0, $perPage);
+                $resource = new Collection($firstPage, new $transformer());
+                $paginator = new LengthAwarePaginator($firstPage, $total, $perPage);
+                $resource->setPaginator(new $paginatorAdapter($paginator));
+            }
+        } else {
+            $resource = new Item($modelInstance, new $transformer());
+        }
 
             $response = response($fractal->createData($resource)->toJson());
 
@@ -128,6 +139,7 @@ class UseTransformerTags extends Strategy
 
         $type = null;
         $states = [];
+        $relations = [];
         if ($modelTag) {
             ['content' => $type, 'attributes' => $attributes] = AnnotationParser::parseIntoContentAndAttributes($modelTag->getContent(), ['states', 'with']);
             $states = $attributes['states'] ? explode(',', $attributes['states']) : [];
@@ -201,4 +213,30 @@ class UseTransformerTags extends Strategy
 
         return Arr::first($transformerTags);
     }
+
+    /**
+     * @param array $tags
+     *
+     * @return array
+     */
+    private function getTransformerPaginatorData(array $tags)
+    {
+        $transformerTags = array_values(
+            array_filter($tags, function ($tag) {
+                return ($tag instanceof Tag) && in_array(strtolower($tag->getName()), ['transformerpaginator']);
+            })
+        );
+
+        $tag = Arr::first($transformerTags);
+        if (empty($tag)) {
+            return ['adapter' => null, 'perPage' => null];
+        }
+
+        $content = $tag->getContent();
+        preg_match('/^\s*(.+?)\s+(\d+)?$/', $content, $result);
+        $paginatorAdapter = $result[1];
+        $perPage = $result[2] ?? null;
+
+        return ['adapter' => $paginatorAdapter, 'perPage' => $perPage];
+    }
 }

+ 83 - 0
tests/Extracting/Strategies/Responses/UseApiResourceTagsTest.php

@@ -94,6 +94,47 @@ class UseApiResourceTagsTest extends TestCase
         ], $results);
     }
 
+    /** @test */
+    public function loads_specified_relations_for_model()
+    {
+        $factory = app(\Illuminate\Database\Eloquent\Factory::class);
+        $factory->afterMaking(TestUser::class, function (TestUser $user, $faker) {
+            if ($user->id === 4) {
+                $child = factory(TestUser::class)->make(['id' => 5, 'parent_id' => 4]);
+                $user->setRelation('children', [$child]);
+            }
+        });
+
+        $config = new DocumentationConfig([]);
+
+        $strategy = new UseApiResourceTags($config);
+        $tags = [
+            new Tag('apiResource', '\Knuckles\Scribe\Tests\Fixtures\TestUserApiResource'),
+            new Tag('apiResourceModel', '\Knuckles\Scribe\Tests\Fixtures\TestUser'),
+        ];
+        $results = $strategy->getApiResourceResponse($tags);
+
+        $this->assertArraySubset([
+            [
+                'status' => 200,
+                'content' => json_encode([
+                    'data' => [
+                        'id' => 4,
+                        'name' => 'Tested Again',
+                        'email' => 'a@b.com',
+                        'children' => [
+                            [
+                                'id' => 5,
+                                'name' => 'Tested Again',
+                                'email' => 'a@b.com',
+                            ],
+                        ],
+                    ],
+                ]),
+            ],
+        ], $results);
+    }
+
     /** @test */
     public function can_parse_apiresourcecollection_tags()
     {
@@ -163,6 +204,48 @@ class UseApiResourceTagsTest extends TestCase
         ], $results);
     }
 
+    /** @test */
+    public function can_parse_apiresourcecollection_tags_with_collection_class_and_pagination()
+    {
+        $config = new DocumentationConfig([]);
+
+        $strategy = new UseApiResourceTags($config);
+        $tags = [
+            new Tag('apiResourceCollection', 'Knuckles\Scribe\Tests\Fixtures\TestUserApiResourceCollection'),
+            new Tag('apiResourceModel', '\Knuckles\Scribe\Tests\Fixtures\TestUser paginate=simple,1'),
+        ];
+        $results = $strategy->getApiResourceResponse($tags);
+
+        $this->assertArraySubset([
+            [
+                'status' => 200,
+                'content' => json_encode([
+                    'data' => [
+                        [
+                            'id' => 4,
+                            'name' => 'Tested Again',
+                            'email' => 'a@b.com',
+                        ],
+                    ],
+                    'links' => [
+                        'self' => 'link-value',
+                        "first" => '/?page=1',
+                        "last" => null,
+                        "prev" => null,
+                        "next" => '/?page=2',
+                    ],
+                    "meta" => [
+                        "current_page" => 1,
+                        "from" => 1,
+                        "path" => '/',
+                        "per_page" => "1",
+                        "to" => 1,
+                    ],
+                ]),
+            ],
+        ], $results);
+    }
+
     public function dataResources()
     {
         return [

+ 108 - 10
tests/Extracting/Strategies/Responses/UseTransformerTagsTest.php

@@ -60,7 +60,13 @@ class UseTransformerTagsTest extends TestCase
         $this->assertArraySubset([
             [
                 'status' => 200,
-                'content' => '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}',
+                'content' => json_encode([
+                    "data" => [
+                        "id" => 1,
+                        "description" => "Welcome on this test versions",
+                        "name" => "TestName",
+                    ],
+                ]),
             ],
         ], $results);
     }
@@ -69,7 +75,9 @@ class UseTransformerTagsTest extends TestCase
     public function can_parse_transformer_tag_with_model_and_factory_states()
     {
         $factory = app(\Illuminate\Database\Eloquent\Factory::class);
-        $factory->define(TestUser::class, function () { return ['id' => 3, 'name' => 'myname']; });
+        $factory->define(TestUser::class, function () {
+            return ['id' => 3, 'name' => 'myname'];
+        });
         $factory->state(TestUser::class, 'state1', ["state1" => true]);
         $factory->state(TestUser::class, 'random-state', ["random-state" => true]);
 
@@ -83,7 +91,14 @@ class UseTransformerTagsTest extends TestCase
         $this->assertArraySubset([
             [
                 'status' => 200,
-                'content' => '{"data":{"id":3,"name":"myname","state1":true,"random-state":true}}',
+                'content' => json_encode([
+                    "data" => [
+                        "id" => 3,
+                        "name" => "myname",
+                        "state1" => true,
+                        "random-state" => true,
+                    ],
+                ]),
             ],
         ], $results);
     }
@@ -100,7 +115,13 @@ class UseTransformerTagsTest extends TestCase
         $this->assertArraySubset([
             [
                 'status' => 201,
-                'content' => '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}',
+                'content' => json_encode([
+                    "data" => [
+                        "id" => 1,
+                        "description" => "Welcome on this test versions",
+                        "name" => "TestName",
+                    ],
+                ]),
             ],
         ], $results);
 
@@ -118,8 +139,20 @@ class UseTransformerTagsTest extends TestCase
         $this->assertArraySubset([
             [
                 'status' => 200,
-                'content' => '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},' .
-                    '{"id":1,"description":"Welcome on this test versions","name":"TestName"}]}',
+                'content' => json_encode([
+                    "data" => [
+                        [
+                            "id" => 1,
+                            "description" => "Welcome on this test versions",
+                            "name" => "TestName",
+                        ],
+                        [
+                            "id" => 1,
+                            "description" => "Welcome on this test versions",
+                            "name" => "TestName",
+                        ],
+                    ],
+                ]),
             ],
         ], $results);
 
@@ -139,8 +172,58 @@ class UseTransformerTagsTest extends TestCase
         $this->assertArraySubset([
             [
                 'status' => 200,
-                'content' => '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},' .
-                    '{"id":1,"description":"Welcome on this test versions","name":"TestName"}]}',
+                'content' => json_encode([
+                    "data" => [
+                        [
+                            "id" => 1,
+                            "description" => "Welcome on this test versions",
+                            "name" => "TestName",
+                        ],
+                        [
+                            "id" => 1,
+                            "description" => "Welcome on this test versions",
+                            "name" => "TestName",
+                        ],
+                    ],
+                ]),
+            ],
+        ], $results);
+    }
+
+    /** @test */
+    public function can_parse_transformercollection_tag_with_model_and_paginator_data()
+    {
+
+        $strategy = new UseTransformerTags(new DocumentationConfig([]));
+        $tags = [
+            new Tag('transformercollection', '\Knuckles\Scribe\Tests\Fixtures\TestTransformer'),
+            new Tag('transformermodel', '\Knuckles\Scribe\Tests\Fixtures\TestModel'),
+            new Tag('transformerpaginator', 'League\Fractal\Pagination\IlluminatePaginatorAdapter 1'),
+        ];
+        $results = $strategy->getTransformerResponse($tags);
+
+        $this->assertArraySubset([
+            [
+                'status' => 200,
+                'content' => json_encode([
+                    "data" => [
+                        [
+                            "id" => 1,
+                            "description" => "Welcome on this test versions",
+                            "name" => "TestName",
+                        ],
+                    ],
+                    'meta' => [
+                        "pagination" => [
+                            "total" => 2,
+                            "count" => 1,
+                            "per_page" => 1,
+                            "current_page" => 1,
+                            "total_pages" => 2,
+                            "links" => ["next" => "/?page=2"],
+                        ],
+                    ],
+                ]),
             ],
         ], $results);
     }
@@ -150,11 +233,26 @@ class UseTransformerTagsTest extends TestCase
         return [
             [
                 null,
-                '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}',
+                json_encode([
+                    "data" => [
+                        "id" => 1,
+                        "description" => "Welcome on this test versions",
+                        "name" => "TestName",
+                    ],
+                ]),
             ],
             [
                 'League\Fractal\Serializer\JsonApiSerializer',
-                '{"data":{"type":null,"id":"1","attributes":{"description":"Welcome on this test versions","name":"TestName"}}}',
+                json_encode([
+                    "data" => [
+                        "type" => null,
+                        "id" => "1",
+                        "attributes" => [
+                            "description" => "Welcome on this test versions",
+                            "name" => "TestName",
+                        ],
+                    ],
+                ]),
             ],
         ];
     }

+ 4 - 0
tests/Fixtures/TestUser.php

@@ -6,4 +6,8 @@ use Illuminate\Database\Eloquent\Model;
 
 class TestUser extends Model
 {
+    public function children()
+    {
+        return $this->hasMany(TestUser::class, 'parent_id');
+    }
 }

+ 3 - 0
tests/Fixtures/TestUserApiResource.php

@@ -19,6 +19,9 @@ class TestUserApiResource extends JsonResource
             'id' => $this->id,
             'name' => $this->first_name . ' ' . $this->last_name,
             'email' => $this->email,
+            'children' => $this->whenLoaded('children', function () {
+                return TestUserApiResource::collection($this->children);
+            }),
         ];
 
         if ($this['state1'] && $this['random-state']) {

+ 5 - 5
tests/GenerateDocumentationTest.php

@@ -354,7 +354,7 @@ class GenerateDocumentationTest extends TestCase
         ]);
         $this->artisan('scribe:generate');
 
-        $generatedMarkdown = $this->getFileContents(__DIR__ . '/../resources/docs/source/groups/0-group-a.md');
+        $generatedMarkdown = $this->getFileContents(__DIR__ . '/../resources/docs/source/groups/group-a.md');
         $this->assertContainsIgnoringWhitespace('"Authorization": "customAuthToken","Custom-Header":"NotSoCustom"', $generatedMarkdown);
     }
 
@@ -366,7 +366,7 @@ class GenerateDocumentationTest extends TestCase
         config(['scribe.routes.0.prefixes' => ['api/*']]);
         $this->artisan('scribe:generate');
 
-        $generatedMarkdown = file_get_contents(__DIR__ . '/../resources/docs/source/groups/0-group-a.md');
+        $generatedMarkdown = file_get_contents(__DIR__ . '/../resources/docs/source/groups/group-a.md');
         $this->assertStringContainsString('Лорем ипсум долор сит амет', $generatedMarkdown);
     }
 
@@ -381,8 +381,8 @@ class GenerateDocumentationTest extends TestCase
         config(['scribe.routes.0.prefixes' => ['api/*']]);
         $this->artisan('scribe:generate');
 
-        $this->assertFileExists(__DIR__ . '/../resources/docs/source/groups/0-1-group-1.md');
-        $this->assertFileExists(__DIR__ . '/../resources/docs/source/groups/1-2-group-2.md');
-        $this->assertFileExists(__DIR__ . '/../resources/docs/source/groups/2-10-group-10.md');
+        $this->assertFileExists(__DIR__ . '/../resources/docs/source/groups/1-group-1.md');
+        $this->assertFileExists(__DIR__ . '/../resources/docs/source/groups/2-group-2.md');
+        $this->assertFileExists(__DIR__ . '/../resources/docs/source/groups/10-group-10.md');
     }
 }