Skip to content

Commit

Permalink
Add support for specifying example model sources
Browse files Browse the repository at this point in the history
  • Loading branch information
shalvah committed Jul 5, 2022
1 parent c850967 commit 39ff208
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 117 deletions.
19 changes: 14 additions & 5 deletions config/scribe.php
Original file line number Diff line number Diff line change
Expand Up @@ -340,11 +340,20 @@
*/
'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.
Expand Down
4 changes: 2 additions & 2 deletions src/Extracting/Extractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,8 @@ public function addAuthField(ExtractedEndpointData $endpointData): void
$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');
Expand Down
61 changes: 61 additions & 0 deletions src/Extracting/InstantiatesExampleModels.php
Original file line number Diff line number Diff line change
@@ -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();
}

}
4 changes: 2 additions & 2 deletions src/Extracting/ParamHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
57 changes: 6 additions & 51 deletions src/Extracting/Strategies/Responses/UseApiResourceTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +12,7 @@
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;
Expand All @@ -29,6 +28,7 @@
class UseApiResourceTags extends Strategy
{
use DatabaseTransactionHelpers;
use InstantiatesExampleModels;

public function __invoke(ExtractedEndpointData $endpointData, array $routeRules): ?array
{
Expand Down Expand Up @@ -69,7 +69,7 @@ public function getApiResourceResponse(Tag $apiResourceTag, array $allTags, Extr
[$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);
Expand All @@ -82,7 +82,7 @@ public function getApiResourceResponse(Tag $apiResourceTag, array $allTags, Extr
// 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)
Expand Down Expand Up @@ -122,7 +122,7 @@ public function getApiResourceResponse(Tag $apiResourceTag, array $allTags, Extr
$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;
})
Expand Down Expand Up @@ -187,6 +187,7 @@ private function getClassToBeTransformedAndAttributes(array $tags): array
* Returns data for simulating JsonResource additional() function
*
* @param Tag|null $tag
*
* @return array
*/
private function getAdditionalData(?Tag $tag): array
Expand All @@ -196,52 +197,6 @@ private function getAdditionalData(?Tag $tag): array
: [];
}

/**
* @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
*
Expand Down
46 changes: 4 additions & 42 deletions src/Extracting/Strategies/Responses/UseTransformerTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,29 @@

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 ).
*/
class UseTransformerTags extends Strategy
{
use DatabaseTransactionHelpers;
use InstantiatesExampleModels;

public function __invoke(ExtractedEndpointData $endpointData, array $routeRules): ?array
{
Expand Down Expand Up @@ -63,7 +62,7 @@ public function getTransformerResponse(array $tags): ?array

[$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();

Expand All @@ -72,7 +71,7 @@ public function getTransformerResponse(array $tags): ?array
}

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);
Expand Down Expand Up @@ -150,43 +149,6 @@ private function getClassToBeTransformed(array $tags, ReflectionFunctionAbstract
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
*
Expand Down
3 changes: 3 additions & 0 deletions tests/Fixtures/TestPet.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

class TestPet extends Model
{
protected $guarded = [];

public $timestamps = false;

public function owners()
{
Expand Down
3 changes: 3 additions & 0 deletions tests/Fixtures/TestUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

class TestUser extends Model
{
protected $guarded = [];

public $timestamps = false;

public function children()
{
Expand Down
12 changes: 1 addition & 11 deletions tests/GenerateDocumentation/OutputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ protected function setUp(): void
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 () {
Expand All @@ -48,16 +48,6 @@ public function tearDown(): void
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');
Expand Down
Loading

0 comments on commit 39ff208

Please sign in to comment.