Browse Source

Add support for specifying example model sources

shalvah 2 years ago
parent
commit
39ff208085

+ 14 - 5
config/scribe.php

@@ -340,11 +340,20 @@ INTRO
      */
     'logo' => false,
 
-    /*
-     * If you would like the package to generate the same example values for parameters on each run,
-     * set this to any number (eg. 1234)
-     */
-    'faker_seed' => null,
+    'examples' => [
+        /*
+         * If you would like the package to generate the same example values for parameters on each run,
+         * set this to any number (eg. 1234)
+         */
+        'faker_seed' => null,
+
+        /*
+         * With API resources and transformers, Scribe tries to generate example models to use in your API responses.
+         * By default, Scribe will try the model's factory, and if that fails, try fetching the first from the database.
+         * You can reorder or remove strategies here.
+         */
+        'models_source' => ['factoryCreate', 'factoryMake', 'database'],
+    ],
 
     /**
      * The strategies Scribe will use to extract information about your routes at each stage.

+ 2 - 2
src/Extracting/Extractor.php

@@ -362,8 +362,8 @@ class Extractor
         $parameterName = $this->config->get('auth.name');
 
         $faker = Factory::create();
-        if ($this->config->get('faker_seed')) {
-            $faker->seed($this->config->get('faker_seed'));
+        if ($seed = $this->config->get('examples.faker_seed')) {
+            $faker->seed($seed);
         }
         $token = $faker->shuffleString('abcdefghkvaZVDPE1864563');
         $valueToUse = $this->config->get('auth.use_value');

+ 61 - 0
src/Extracting/InstantiatesExampleModels.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Knuckles\Scribe\Extracting;
+
+use Illuminate\Database\Eloquent\Model;
+use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
+use Knuckles\Scribe\Tools\ErrorHandlingUtils as e;
+use Knuckles\Scribe\Tools\Utils;
+use Throwable;
+
+trait InstantiatesExampleModels
+{
+    /**
+     * @param string $type
+     *
+     * @param array $relations
+     * @param array $factoryStates
+     *
+     * @return Model|object
+     */
+    protected function instantiateExampleModel(string $type, array $factoryStates = [], array $relations = [])
+    {
+        $configuredStrategies = $this->config->get('examples.models_source', ['factoryCreate', 'factoryMake', 'database']);
+
+        $strategies = [
+            'factoryCreate' => fn() => $this->getExampleModelFromFactoryCreate($type, $factoryStates, $relations),
+            'factoryMake' => fn() => $this->getExampleModelFromFactoryMake($type, $factoryStates, $relations),
+            'database' => fn() => $this->getExampleModelFromDatabase($type, $relations),
+        ];
+
+        foreach ($configuredStrategies as $strategyName) {
+            try {
+                $model = $strategies[$strategyName]();
+                if ($model) return $model;
+            } catch (Throwable $e) {
+                c::warn("Couldn't get example model for {$type} via $strategyName.");
+                e::dumpExceptionIfVerbose($e, true);
+            }
+        }
+
+        return new $type;
+    }
+
+    protected function getExampleModelFromFactoryCreate(string $type, array $factoryStates = [], array $relations = [])
+    {
+        $factory = Utils::getModelFactory($type, $factoryStates, $relations);
+        return $factory->create()->load($relations);
+    }
+
+    protected function getExampleModelFromFactoryMake(string $type, array $factoryStates = [], array $relations = [])
+    {
+        $factory = Utils::getModelFactory($type, $factoryStates, $relations);
+        return $factory->make();
+    }
+
+    protected function getExampleModelFromDatabase(string $type, array $relations = [])
+    {
+        return $type::with($relations)->first();
+    }
+
+}

+ 2 - 2
src/Extracting/ParamHelpers.php

@@ -12,8 +12,8 @@ trait ParamHelpers
     protected function getFaker(): \Faker\Generator
     {
         $faker = Factory::create();
-        if ($this->config->get('faker_seed')) {
-            $faker->seed($this->config->get('faker_seed'));
+        if ($seed = $this->config->get('examples.faker_seed')) {
+            $faker->seed($seed);
         }
         return $faker;
     }

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

@@ -2,10 +2,8 @@
 
 namespace Knuckles\Scribe\Extracting\Strategies\Responses;
 
-use Illuminate\Support\Str;
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Exception;
-use Illuminate\Database\Eloquent\Model;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 use Illuminate\Http\Resources\Json\ResourceCollection;
@@ -14,6 +12,7 @@ use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Pagination\Paginator;
 use Illuminate\Support\Arr;
 use Knuckles\Scribe\Extracting\DatabaseTransactionHelpers;
+use Knuckles\Scribe\Extracting\InstantiatesExampleModels;
 use Knuckles\Scribe\Extracting\RouteDocBlocker;
 use Knuckles\Scribe\Extracting\Strategies\Strategy;
 use Knuckles\Scribe\Tools\AnnotationParser as a;
@@ -29,6 +28,7 @@ use Throwable;
 class UseApiResourceTags extends Strategy
 {
     use DatabaseTransactionHelpers;
+    use InstantiatesExampleModels;
 
     public function __invoke(ExtractedEndpointData $endpointData, array $routeRules): ?array
     {
@@ -69,7 +69,7 @@ class UseApiResourceTags extends Strategy
         [$statusCode, $apiResourceClass, $description] = $this->getStatusCodeAndApiResourceClass($apiResourceTag);
         [$model, $factoryStates, $relations, $pagination] = $this->getClassToBeTransformedAndAttributes($allTags);
         $additionalData = $this->getAdditionalData($this->getApiResourceAdditionalTag($allTags));
-        $modelInstance = $this->instantiateApiResourceModel($model, $factoryStates, $relations);
+        $modelInstance = $this->instantiateExampleModel($model, $factoryStates, $relations);
 
         try {
             $resource = new $apiResourceClass($modelInstance);
@@ -82,7 +82,7 @@ class UseApiResourceTags extends Strategy
             // Collections can either use the regular JsonResource class (via `::collection()`,
             // or a ResourceCollection (via `new`)
             // See https://laravel.com/docs/5.8/eloquent-resources
-            $models = [$modelInstance, $this->instantiateApiResourceModel($model, $factoryStates, $relations)];
+            $models = [$modelInstance, $this->instantiateExampleModel($model, $factoryStates, $relations)];
             // Pagination can be in two forms:
             // [15] : means ::paginate(15)
             // [15, 'simple'] : means ::simplePaginate(15)
@@ -122,7 +122,7 @@ class UseApiResourceTags extends Strategy
         $route = $endpointData->route;
         /** @var Response $response */
         $response = $resource->toResponse(
-            // Set the route properly so it works for users who have code that checks for the route.
+        // Set the route properly so it works for users who have code that checks for the route.
             $request->setRouteResolver(function () use ($route) {
                 return $route;
             })
@@ -187,6 +187,7 @@ class UseApiResourceTags extends Strategy
      * Returns data for simulating JsonResource additional() function
      *
      * @param Tag|null $tag
+     *
      * @return array
      */
     private function getAdditionalData(?Tag $tag): array
@@ -196,52 +197,6 @@ class UseApiResourceTags extends Strategy
             : [];
     }
 
-    /**
-     * @param string $type
-     *
-     * @param array $relations
-     * @param array $factoryStates
-     *
-     * @return Model|object
-     */
-    protected function instantiateApiResourceModel(string $type, array $factoryStates = [], array $relations = [])
-    {
-        try {
-            // Try Eloquent model factory
-            $factory = Utils::getModelFactory($type, $factoryStates, $relations);
-
-            try {
-                return $factory->create()->load($relations);
-            } catch (Throwable $e) {
-                c::warn("Eloquent model factory failed to create {$type}; trying to make it.");
-                e::dumpExceptionIfVerbose($e, true);
-
-                // If there was no working database, ->create() would fail. Try ->make() instead
-                return $factory->make();
-            }
-        } catch (Throwable $e) {
-            c::warn("Eloquent model factory failed to instantiate {$type}; trying to fetch from database.");
-            e::dumpExceptionIfVerbose($e, true);
-
-            $instance = new $type();
-            if ($instance instanceof \Illuminate\Database\Eloquent\Model) {
-                try {
-                    // We can't use a factory but can try to get one from the database
-                    $firstInstance = $type::with($relations)->first();
-                    if ($firstInstance) {
-                        return $firstInstance;
-                    }
-                } catch (Throwable $e) {
-                    // okay, we'll stick with `new`
-                    c::warn("Failed to fetch first {$type} from database; using `new` to instantiate.");
-                    e::dumpExceptionIfVerbose($e);
-                }
-            }
-        }
-
-        return $instance;
-    }
-
     /**
      * @param Tag[] $tags
      *

+ 4 - 42
src/Extracting/Strategies/Responses/UseTransformerTags.php

@@ -4,23 +4,21 @@ namespace Knuckles\Scribe\Extracting\Strategies\Responses;
 
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Exception;
-use Illuminate\Database\Eloquent\Model as IlluminateModel;
 use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Arr;
 use Knuckles\Scribe\Extracting\DatabaseTransactionHelpers;
+use Knuckles\Scribe\Extracting\InstantiatesExampleModels;
 use Knuckles\Scribe\Extracting\RouteDocBlocker;
 use Knuckles\Scribe\Extracting\Strategies\Strategy;
 use Knuckles\Scribe\Tools\AnnotationParser as a;
 use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
 use Knuckles\Scribe\Tools\ErrorHandlingUtils as e;
-use Knuckles\Scribe\Tools\Utils;
 use League\Fractal\Manager;
 use League\Fractal\Resource\Collection;
 use League\Fractal\Resource\Item;
 use Mpociot\Reflection\DocBlock\Tag;
 use ReflectionClass;
 use ReflectionFunctionAbstract;
-use Throwable;
 
 /**
  * Parse a transformer response from the docblock ( @transformer || @transformercollection ).
@@ -28,6 +26,7 @@ use Throwable;
 class UseTransformerTags extends Strategy
 {
     use DatabaseTransactionHelpers;
+    use InstantiatesExampleModels;
 
     public function __invoke(ExtractedEndpointData $endpointData, array $routeRules): ?array
     {
@@ -63,7 +62,7 @@ class UseTransformerTags extends Strategy
 
         [$statusCode, $transformer] = $this->getStatusCodeAndTransformerClass($transformerTag);
         [$model, $factoryStates, $relations, $resourceKey] = $this->getClassToBeTransformed($tags, (new ReflectionClass($transformer))->getMethod('transform'));
-        $modelInstance = $this->instantiateTransformerModel($model, $factoryStates, $relations);
+        $modelInstance = $this->instantiateExampleModel($model, $factoryStates, $relations);
 
         $fractal = new Manager();
 
@@ -72,7 +71,7 @@ class UseTransformerTags extends Strategy
         }
 
         if ((strtolower($transformerTag->getName()) == 'transformercollection')) {
-            $models = [$modelInstance, $this->instantiateTransformerModel($model, $factoryStates, $relations)];
+            $models = [$modelInstance, $this->instantiateExampleModel($model, $factoryStates, $relations)];
             $resource = new Collection($models, new $transformer());
 
             ['adapter' => $paginatorAdapter, 'perPage' => $perPage] = $this->getTransformerPaginatorData($tags);
@@ -150,43 +149,6 @@ class UseTransformerTags extends Strategy
         return [$type, $states, $relations, $resourceKey];
     }
 
-    protected function instantiateTransformerModel(string $type, array $factoryStates = [], array $relations = [])
-    {
-        try {
-            // try Eloquent model factory
-
-            /** @var \Illuminate\Database\Eloquent\Factories\Factory $factory */
-            $factory = Utils::getModelFactory($type, $factoryStates, $relations);
-
-            try {
-                return $factory->create();
-            } catch (Throwable $e) {
-                // If there was no working database, ->create() would fail. Try ->make() instead
-                return $factory->make();
-            }
-        } catch (Throwable $e) {
-            c::warn("Eloquent model factory failed to instantiate {$type}; trying to fetch from database.");
-            e::dumpExceptionIfVerbose($e, true);
-
-            $instance = new $type();
-            if ($instance instanceof IlluminateModel) {
-                try {
-                    // We can't use a factory but can try to get one from the database
-                    $firstInstance = $type::with($relations)->first();
-                    if ($firstInstance) {
-                        return $firstInstance;
-                    }
-                } catch (Throwable $e) {
-                    // okay, we'll stick with `new`
-                    c::warn("Failed to fetch first {$type} from database; using `new` to instantiate.");
-                    e::dumpExceptionIfVerbose($e);
-                }
-            }
-        }
-
-        return $instance;
-    }
-
     /**
      * @param Tag[] $tags
      *

+ 3 - 0
tests/Fixtures/TestPet.php

@@ -6,6 +6,9 @@ use Illuminate\Database\Eloquent\Model;
 
 class TestPet extends Model
 {
+    protected $guarded = [];
+
+    public $timestamps = false;
 
     public function owners()
     {

+ 3 - 0
tests/Fixtures/TestUser.php

@@ -6,6 +6,9 @@ use Illuminate\Database\Eloquent\Model;
 
 class TestUser extends Model
 {
+    protected $guarded = [];
+
+    public $timestamps = false;
 
     public function children()
     {

+ 1 - 11
tests/GenerateDocumentation/OutputTest.php

@@ -29,7 +29,7 @@ class OutputTest extends BaseLaravelTest
         config(['scribe.openapi.enabled' => false]);
         config(['scribe.postman.enabled' => false]);
         // We want to have the same values for params each time
-        config(['scribe.faker_seed' => 1234]);
+        config(['scribe.examples.faker_seed' => 1234]);
 
         $factory = app(\Illuminate\Database\Eloquent\Factory::class);
         $factory->define(TestUser::class, function () {
@@ -48,16 +48,6 @@ class OutputTest extends BaseLaravelTest
         Utils::deleteDirectoryAndContents('.scribe');
     }
 
-    protected function defineEnvironment($app)
-    {
-        $app['config']->set('database.default', 'testbench');
-        $app['config']->set('database.connections.testbench', [
-            'driver'   => 'sqlite',
-            'database' => ':memory:',
-            'prefix'   => '',
-        ]);
-    }
-
     protected function usingLaravelTypeDocs($app)
     {
         $app['config']->set('scribe.type', 'laravel');

+ 50 - 0
tests/Strategies/Responses/UseApiResourceTagsTest.php

@@ -2,7 +2,9 @@
 
 namespace Knuckles\Scribe\Tests\Strategies\Responses;
 
+use Illuminate\Database\Schema\Blueprint;
 use Illuminate\Routing\Route;
+use Illuminate\Support\Facades\Schema;
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Knuckles\Scribe\Extracting\Strategies\Responses\UseApiResourceTags;
 use Knuckles\Scribe\ScribeServiceProvider;
@@ -59,6 +61,16 @@ class UseApiResourceTagsTest extends BaseLaravelTest
         });
     }
 
+    protected function usingDatabase($app)
+    {
+        $app['config']->set('database.default', 'sqlite');
+        $app['config']->set('database.connections.sqlite', [
+            'driver'   => 'sqlite',
+            'database' => ':memory:',
+            'prefix'   => '',
+        ]);
+    }
+
     /** @test */
     public function can_parse_apiresource_tags()
     {
@@ -87,6 +99,44 @@ class UseApiResourceTagsTest extends BaseLaravelTest
         ], $results);
     }
 
+    /**
+     * @test
+     * @define-env usingDatabase
+     */
+    public function respects_models_source_settings()
+    {
+        $config = new DocumentationConfig(['examples' => ['models_source' => ['database', 'factoryMake']]]);
+        $route = new Route(['POST'], "/somethingRandom", ['uses' => [TestController::class, 'dummy']]);
+
+        $strategy = new UseApiResourceTags($config);
+        $tags = [
+            new Tag('apiResource', '\Knuckles\Scribe\Tests\Fixtures\TestUserApiResource'),
+            new Tag('apiResourceModel', '\Knuckles\Scribe\Tests\Fixtures\TestUser'),
+        ];
+
+        Schema::create('test_users', function (Blueprint $table) {
+            $table->id();
+            $table->string('first_name');
+            $table->string('last_name');
+            $table->string('email');
+        });
+        TestUser::create(['first_name' => 'Testy', 'last_name' => 'Testes', 'email' => 'um']);
+
+        $results = $strategy->getApiResourceResponse($strategy->getApiResourceTag($tags), $tags, ExtractedEndpointData::fromRoute($route));
+
+        $this->assertArraySubset([
+            [
+                'status' => 200,
+                'content' => json_encode([
+                    'data' => [
+                        'id' => 1,
+                        'name' => 'Testy Testes',
+                        'email' => 'um'
+                    ],
+                ]),
+            ],
+        ], $results);
+    }
 
     /** @test */
     public function can_parse_apiresource_tags_with_scenario_and_status_attributes()

+ 3 - 4
tests/Unit/ExtractorTest.php

@@ -48,7 +48,6 @@ class ExtractorTest extends TestCase
                 \Knuckles\Scribe\Extracting\Strategies\ResponseFields\GetFromResponseFieldTag::class,
             ],
         ],
-        'groups.default' => 'general',
     ];
 
     public static $globalValue = null;
@@ -216,16 +215,16 @@ class ExtractorTest extends TestCase
         $results[$this->generator->processRoute($route)->cleanBodyParameters[$paramName]] = true;
         $results[$this->generator->processRoute($route)->cleanBodyParameters[$paramName]] = true;
         // Examples should have different values
-        $this->assertNotEquals(count($results), 1);
+        $this->assertNotEquals(1, count($results));
 
-        $generator = new Extractor(new DocumentationConfig($this->config + ['faker_seed' => 12345]));
+        $generator = new Extractor(new DocumentationConfig($this->config + ['examples' => ['faker_seed' => 12345]]));
         $results = [];
         $results[$generator->processRoute($route)->cleanBodyParameters[$paramName]] = true;
         $results[$generator->processRoute($route)->cleanBodyParameters[$paramName]] = true;
         $results[$generator->processRoute($route)->cleanBodyParameters[$paramName]] = true;
         $results[$generator->processRoute($route)->cleanBodyParameters[$paramName]] = true;
         // Examples should have same values
-        $this->assertEquals(count($results), 1);
+        $this->assertEquals(1, count($results));
     }
 
     /** @test */