update after stop build
All checks were successful
All checks were successful
This commit is contained in:
16
vendor/knuckleswtf/scribe/src/Attributes/Authenticated.php
vendored
Normal file
16
vendor/knuckleswtf/scribe/src/Attributes/Authenticated.php
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class Authenticated
|
||||
{
|
||||
public function __construct(
|
||||
public ?bool $authenticated = true,
|
||||
) {}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return ['authenticated' => $this->authenticated];
|
||||
}
|
||||
}
|
||||
6
vendor/knuckleswtf/scribe/src/Attributes/BodyParam.php
vendored
Normal file
6
vendor/knuckleswtf/scribe/src/Attributes/BodyParam.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class BodyParam extends GenericParam {}
|
||||
16
vendor/knuckleswtf/scribe/src/Attributes/Deprecated.php
vendored
Normal file
16
vendor/knuckleswtf/scribe/src/Attributes/Deprecated.php
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class Deprecated
|
||||
{
|
||||
public function __construct(
|
||||
public bool|string|null $deprecated = true,
|
||||
) {}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return ['deprecated' => $this->deprecated];
|
||||
}
|
||||
}
|
||||
29
vendor/knuckleswtf/scribe/src/Attributes/Endpoint.php
vendored
Normal file
29
vendor/knuckleswtf/scribe/src/Attributes/Endpoint.php
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
|
||||
class Endpoint
|
||||
{
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public ?string $description = '',
|
||||
/** You can use the separate #[Authenticated] attribute, or pass authenticated: false to this. */
|
||||
public ?bool $authenticated = null,
|
||||
) {}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$data = [
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
];
|
||||
if (! is_null($this->authenticated)) {
|
||||
$data['authenticated'] = $this->authenticated;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
57
vendor/knuckleswtf/scribe/src/Attributes/GenericParam.php
vendored
Normal file
57
vendor/knuckleswtf/scribe/src/Attributes/GenericParam.php
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class GenericParam
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?string $type = 'string',
|
||||
public ?string $description = '',
|
||||
public ?bool $required = true,
|
||||
public mixed $example = null, // Pass 'No-example' to omit the example
|
||||
public mixed $enum = null, // Can pass a list of values, or a native PHP enum
|
||||
public ?bool $nullable = false,
|
||||
public ?bool $deprecated = false,
|
||||
) {}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'type' => $this->type,
|
||||
'required' => $this->required,
|
||||
'example' => $this->example,
|
||||
'enumValues' => $this->getEnumValues(),
|
||||
'nullable' => $this->nullable,
|
||||
'deprecated' => $this->deprecated,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getEnumValues(): array
|
||||
{
|
||||
if (! $this->enum) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_array($this->enum)) {
|
||||
return $this->enum;
|
||||
}
|
||||
|
||||
if (enum_exists($this->enum) && method_exists($this->enum, 'tryFrom')) {
|
||||
return array_map(
|
||||
// $case->value only exists on BackedEnums, not UnitEnums
|
||||
// method_exists($enum, 'tryFrom') implies the enum is a BackedEnum
|
||||
// @phpstan-ignore-next-line
|
||||
fn ($case) => $case->value,
|
||||
$this->enum::cases()
|
||||
);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(
|
||||
'The enum property of a parameter must be either a PHP enum or an array of values'
|
||||
);
|
||||
}
|
||||
}
|
||||
45
vendor/knuckleswtf/scribe/src/Attributes/Group.php
vendored
Normal file
45
vendor/knuckleswtf/scribe/src/Attributes/Group.php
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
|
||||
class Group
|
||||
{
|
||||
public function __construct(
|
||||
public mixed $name,
|
||||
public ?string $description = '',
|
||||
/** You can use the separate #[Authenticated] attribute, or pass authenticated: false to this. */
|
||||
public ?bool $authenticated = null,
|
||||
) {}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
$data = [
|
||||
'groupName' => $this->getName(),
|
||||
'groupDescription' => $this->description,
|
||||
];
|
||||
|
||||
if (! is_null($this->authenticated)) {
|
||||
$data['authenticated'] = $this->authenticated;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getName(): string
|
||||
{
|
||||
if (is_string($this->name)) {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
if (interface_exists('BackedEnum') && is_a($this->name, 'BackedEnum')) {
|
||||
return $this->name->value;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(
|
||||
'The name property of a group must be either a PHP Backed Enum or a string'
|
||||
);
|
||||
}
|
||||
}
|
||||
20
vendor/knuckleswtf/scribe/src/Attributes/Header.php
vendored
Normal file
20
vendor/knuckleswtf/scribe/src/Attributes/Header.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class Header
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public mixed $example = null,
|
||||
) {}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'example' => $this->example,
|
||||
];
|
||||
}
|
||||
}
|
||||
6
vendor/knuckleswtf/scribe/src/Attributes/QueryParam.php
vendored
Normal file
6
vendor/knuckleswtf/scribe/src/Attributes/QueryParam.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class QueryParam extends GenericParam {}
|
||||
26
vendor/knuckleswtf/scribe/src/Attributes/Response.php
vendored
Normal file
26
vendor/knuckleswtf/scribe/src/Attributes/Response.php
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class Response
|
||||
{
|
||||
public function __construct(
|
||||
public array|string|null $content = null,
|
||||
public int $status = 200,
|
||||
public ?string $description = '',
|
||||
) {}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'status' => $this->status,
|
||||
'content' => match (true) {
|
||||
is_null($this->content) => null,
|
||||
is_string($this->content) => $this->content,
|
||||
default => json_encode($this->content, JSON_THROW_ON_ERROR),
|
||||
},
|
||||
'description' => $this->description,
|
||||
];
|
||||
}
|
||||
}
|
||||
20
vendor/knuckleswtf/scribe/src/Attributes/ResponseField.php
vendored
Normal file
20
vendor/knuckleswtf/scribe/src/Attributes/ResponseField.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class ResponseField extends GenericParam
|
||||
{
|
||||
// Don't default to string; type inference is currently handled by the normalizer
|
||||
// TODO change this in the future
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?string $type = null,
|
||||
public ?string $description = '',
|
||||
public ?bool $required = true,
|
||||
public mixed $example = null, // Pass 'No-example' to omit the example
|
||||
public mixed $enum = null, // Can pass a list of values, or a native PHP enum,
|
||||
public ?bool $nullable = false,
|
||||
public ?bool $deprecated = false,
|
||||
) {}
|
||||
}
|
||||
45
vendor/knuckleswtf/scribe/src/Attributes/ResponseFromApiResource.php
vendored
Normal file
45
vendor/knuckleswtf/scribe/src/Attributes/ResponseFromApiResource.php
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Knuckles\Scribe\Extracting\Shared\ApiResourceResponseTools;
|
||||
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class ResponseFromApiResource
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?string $model = null,
|
||||
public int $status = 200,
|
||||
public ?string $description = '',
|
||||
|
||||
// Mark if this should be used as a collection. Only needed if not using a ResourceCollection.
|
||||
public ?bool $collection = null,
|
||||
public array $factoryStates = [],
|
||||
public array $with = [],
|
||||
public ?int $paginate = null,
|
||||
public ?int $simplePaginate = null,
|
||||
public ?int $cursorPaginate = null,
|
||||
public array $additional = [],
|
||||
public array $withCount = [],
|
||||
) {}
|
||||
|
||||
public function modelToBeTransformed(): ?string
|
||||
{
|
||||
if (! empty($this->model)) {
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
return ApiResourceResponseTools::tryToInferApiResourceModel($this->name);
|
||||
}
|
||||
|
||||
public function isCollection(): bool
|
||||
{
|
||||
if (! is_null($this->collection)) {
|
||||
return $this->collection;
|
||||
}
|
||||
|
||||
return is_subclass_of($this->name, ResourceCollection::class);
|
||||
}
|
||||
}
|
||||
25
vendor/knuckleswtf/scribe/src/Attributes/ResponseFromFile.php
vendored
Normal file
25
vendor/knuckleswtf/scribe/src/Attributes/ResponseFromFile.php
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
use Knuckles\Scribe\Extracting\Shared\ResponseFileTools;
|
||||
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class ResponseFromFile
|
||||
{
|
||||
public function __construct(
|
||||
public string $file,
|
||||
public int $status = 200,
|
||||
public array|string $merge = [],
|
||||
public ?string $description = '',
|
||||
) {}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'status' => $this->status,
|
||||
'description' => $this->description,
|
||||
'content' => ResponseFileTools::getResponseContents($this->file, $this->merge),
|
||||
];
|
||||
}
|
||||
}
|
||||
23
vendor/knuckleswtf/scribe/src/Attributes/ResponseFromTransformer.php
vendored
Normal file
23
vendor/knuckleswtf/scribe/src/Attributes/ResponseFromTransformer.php
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class ResponseFromTransformer
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?string $model = null,
|
||||
public int $status = 200,
|
||||
public ?string $description = '',
|
||||
|
||||
// Mark if this should be used as a collection. Only needed if not using a CollectionTransformer.
|
||||
public bool $collection = false,
|
||||
public array $factoryStates = [],
|
||||
public array $with = [],
|
||||
public ?string $resourceKey = null,
|
||||
|
||||
// Format: [adapter, numberPerPage]. Example: [SomePaginator::class, 10]
|
||||
public array $paginate = [],
|
||||
) {}
|
||||
}
|
||||
35
vendor/knuckleswtf/scribe/src/Attributes/Subgroup.php
vendored
Normal file
35
vendor/knuckleswtf/scribe/src/Attributes/Subgroup.php
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class Subgroup
|
||||
{
|
||||
public function __construct(
|
||||
public mixed $name,
|
||||
public ?string $description = '',
|
||||
) {}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'subgroup' => $this->getName(),
|
||||
'subgroupDescription' => $this->description,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getName(): string
|
||||
{
|
||||
if (is_string($this->name)) {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
if (interface_exists('BackedEnum') && is_a($this->name, 'BackedEnum')) {
|
||||
return $this->name->value;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(
|
||||
'The name property of a subgroup must be either a PHP Backed Enum or a string'
|
||||
);
|
||||
}
|
||||
}
|
||||
12
vendor/knuckleswtf/scribe/src/Attributes/Unauthenticated.php
vendored
Normal file
12
vendor/knuckleswtf/scribe/src/Attributes/Unauthenticated.php
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class Unauthenticated
|
||||
{
|
||||
public function toArray()
|
||||
{
|
||||
return ['authenticated' => false];
|
||||
}
|
||||
}
|
||||
6
vendor/knuckleswtf/scribe/src/Attributes/UrlParam.php
vendored
Normal file
6
vendor/knuckleswtf/scribe/src/Attributes/UrlParam.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Attributes;
|
||||
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
|
||||
class UrlParam extends GenericParam {}
|
||||
35
vendor/knuckleswtf/scribe/src/Commands/DiffConfig.php
vendored
Normal file
35
vendor/knuckleswtf/scribe/src/Commands/DiffConfig.php
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Knuckles\Scribe\Tools\ConfigDiffer;
|
||||
|
||||
class DiffConfig extends Command
|
||||
{
|
||||
protected $signature = 'scribe:config:diff {--config=scribe : choose which config file to use}';
|
||||
|
||||
protected $description = 'Dump your changed config to the console. Use this when posting bug reports';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$usersConfig = config($this->option('config'));
|
||||
$defaultConfig = require __DIR__.'/../../config/scribe.php';
|
||||
|
||||
$ignore = ['example_languages', 'routes', 'description', 'auth.extra_info', 'intro_text', 'groups'];
|
||||
$asList = ['strategies.*', 'examples.models_source'];
|
||||
$differ = new ConfigDiffer(original: $defaultConfig, changed: $usersConfig, ignorePaths: $ignore, asList: $asList);
|
||||
|
||||
$diff = $differ->getDiff();
|
||||
|
||||
if (empty($diff)) {
|
||||
$this->info('------ SAME AS DEFAULT CONFIG ------');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($diff as $key => $item) {
|
||||
$this->line("{$key} => {$item}");
|
||||
}
|
||||
}
|
||||
}
|
||||
257
vendor/knuckleswtf/scribe/src/Commands/GenerateDocumentation.php
vendored
Normal file
257
vendor/knuckleswtf/scribe/src/Commands/GenerateDocumentation.php
vendored
Normal file
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Console\View\Components\Factory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Camel\Camel;
|
||||
use Knuckles\Scribe\GroupedEndpoints\GroupedEndpointsFactory;
|
||||
use Knuckles\Scribe\Matching\RouteMatcherInterface;
|
||||
use Knuckles\Scribe\Scribe;
|
||||
use Knuckles\Scribe\Tools\ConfigDiffer;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\DocumentationConfig;
|
||||
use Knuckles\Scribe\Tools\ErrorHandlingUtils as e;
|
||||
use Knuckles\Scribe\Tools\Globals;
|
||||
use Knuckles\Scribe\Tools\PathConfig;
|
||||
use Knuckles\Scribe\Writing\Writer;
|
||||
|
||||
class GenerateDocumentation extends Command
|
||||
{
|
||||
protected $signature = "scribe:generate
|
||||
{--force : Discard any changes you've made to the YAML or Markdown files}
|
||||
{--no-extraction : Skip extraction of route and API info and just transform the YAML and Markdown files into HTML}
|
||||
{--no-upgrade-check : Skip checking for config file upgrades. Won't make things faster, but can be helpful if the command is buggy}
|
||||
{--config=scribe : Choose which config file to use}
|
||||
{--scribe-dir= : Specify the directory where Scribe stores its intermediate output and cache. Defaults to `.<config_file>`}
|
||||
";
|
||||
|
||||
protected $description = 'Generate API documentation from your Laravel routes.';
|
||||
|
||||
protected DocumentationConfig $docConfig;
|
||||
|
||||
protected bool $shouldExtract;
|
||||
|
||||
protected bool $forcing;
|
||||
|
||||
protected PathConfig $paths;
|
||||
|
||||
public function handle(RouteMatcherInterface $routeMatcher, GroupedEndpointsFactory $groupedEndpointsFactory): void
|
||||
{
|
||||
$this->bootstrap();
|
||||
|
||||
if (! empty($this->docConfig->get('default_group'))) {
|
||||
$this->warn('It looks like you just upgraded to Scribe v4.');
|
||||
$this->warn('Please run the upgrade command first: `php artisan scribe:upgrade`.');
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Extraction stage - extract endpoint info either from app or existing Camel files (previously extracted data)
|
||||
$groupedEndpointsInstance = $groupedEndpointsFactory->make($this, $routeMatcher, $this->paths);
|
||||
$extractedEndpoints = $groupedEndpointsInstance->get();
|
||||
$userDefinedEndpoints = Camel::loadUserDefinedEndpoints(Camel::camelDir($this->paths));
|
||||
$groupedEndpoints = $this->mergeUserDefinedEndpoints($extractedEndpoints, $userDefinedEndpoints);
|
||||
|
||||
// Output stage
|
||||
$configFileOrder = $this->docConfig->get('groups.order', []);
|
||||
$groupedEndpoints = Camel::prepareGroupedEndpointsForOutput($groupedEndpoints, $configFileOrder);
|
||||
|
||||
if (! count($userDefinedEndpoints)) {
|
||||
// Update the example custom file if there were no custom endpoints
|
||||
$this->writeExampleCustomEndpoint();
|
||||
}
|
||||
|
||||
/** @var Writer $writer */
|
||||
$writer = app(Writer::class, ['config' => $this->docConfig, 'paths' => $this->paths]);
|
||||
$writer->writeDocs($groupedEndpoints);
|
||||
|
||||
// Retiring the automatic upgrade check, since the config file is no longer changing as frequently.
|
||||
// $this->upgradeConfigFileIfNeeded();
|
||||
|
||||
$this->sayGoodbye(errored: $groupedEndpointsInstance->hasEncounteredErrors());
|
||||
}
|
||||
|
||||
public function isForcing(): bool
|
||||
{
|
||||
return $this->forcing;
|
||||
}
|
||||
|
||||
public function shouldExtract(): bool
|
||||
{
|
||||
return $this->shouldExtract;
|
||||
}
|
||||
|
||||
public function getDocConfig(): DocumentationConfig
|
||||
{
|
||||
return $this->docConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access to output components for task-based output.
|
||||
*
|
||||
* @return Factory
|
||||
*/
|
||||
public function outputComponents()
|
||||
{
|
||||
return $this->components;
|
||||
}
|
||||
|
||||
public function bootstrap(): void
|
||||
{
|
||||
// The --verbose option is included with all Artisan commands.
|
||||
Globals::$shouldBeVerbose = $this->option('verbose');
|
||||
|
||||
c::bootstrapOutput($this->output);
|
||||
c::setCommand($this);
|
||||
|
||||
$configName = $this->option('config');
|
||||
if (! config($configName)) {
|
||||
throw new \InvalidArgumentException("The specified config (config/{$configName}.php) doesn't exist.");
|
||||
}
|
||||
|
||||
$this->paths = new PathConfig($configName);
|
||||
if ($this->hasOption('scribe-dir') && ! empty($this->option('scribe-dir'))) {
|
||||
$this->paths = new PathConfig(
|
||||
$configName,
|
||||
scribeDir: $this->option('scribe-dir')
|
||||
);
|
||||
}
|
||||
|
||||
$this->docConfig = new DocumentationConfig(config($this->paths->configName));
|
||||
|
||||
// Force root URL so it works in Postman collection
|
||||
$baseUrl = $this->docConfig->get('base_url') ?? config('app.url');
|
||||
|
||||
try {
|
||||
// Renamed from forceRootUrl in Laravel 11.43 or so
|
||||
URL::useOrigin($baseUrl);
|
||||
} catch (\BadMethodCallException) {
|
||||
URL::forceRootUrl($baseUrl);
|
||||
}
|
||||
|
||||
$this->forcing = $this->option('force');
|
||||
$this->shouldExtract = ! $this->option('no-extraction');
|
||||
|
||||
if ($this->forcing && ! $this->shouldExtract) {
|
||||
throw new \InvalidArgumentException("Can't use --force and --no-extraction together.");
|
||||
}
|
||||
|
||||
$this->runBootstrapHook();
|
||||
}
|
||||
|
||||
protected function runBootstrapHook()
|
||||
{
|
||||
if (is_callable(Globals::$__bootstrap)) {
|
||||
call_user_func_array(Globals::$__bootstrap, [$this]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function mergeUserDefinedEndpoints(array $groupedEndpoints, array $userDefinedEndpoints): array
|
||||
{
|
||||
foreach ($userDefinedEndpoints as $endpoint) {
|
||||
$indexOfGroupWhereThisEndpointShouldBeAdded = Arr::first(array_keys($groupedEndpoints), function ($key) use ($groupedEndpoints, $endpoint) {
|
||||
$group = $groupedEndpoints[$key];
|
||||
|
||||
return $group['name'] === ($endpoint['metadata']['groupName'] ?? $this->docConfig->get('groups.default', ''));
|
||||
});
|
||||
|
||||
if ($indexOfGroupWhereThisEndpointShouldBeAdded !== null) {
|
||||
$groupedEndpoints[$indexOfGroupWhereThisEndpointShouldBeAdded]['endpoints'][] = $endpoint;
|
||||
} else {
|
||||
$newGroup = [
|
||||
'name' => $endpoint['metadata']['groupName'] ?? $this->docConfig->get('groups.default', ''),
|
||||
'description' => $endpoint['metadata']['groupDescription'] ?? null,
|
||||
'endpoints' => [$endpoint],
|
||||
];
|
||||
|
||||
$groupedEndpoints[$newGroup['name']] = $newGroup;
|
||||
}
|
||||
}
|
||||
|
||||
return $groupedEndpoints;
|
||||
}
|
||||
|
||||
protected function writeExampleCustomEndpoint(): void
|
||||
{
|
||||
// We add an example to guide users in case they need to add a custom endpoint.
|
||||
copy(__DIR__.'/../../resources/example_custom_endpoint.yaml', Camel::camelDir($this->paths).'/custom.0.yaml');
|
||||
}
|
||||
|
||||
protected function upgradeConfigFileIfNeeded(): void
|
||||
{
|
||||
if ($this->option('no-upgrade-check')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info('Checking for any pending upgrades to your config file...');
|
||||
|
||||
try {
|
||||
$defaultConfig = require __DIR__.'/../../config/scribe.php';
|
||||
$ignore = ['example_languages', 'routes', 'description', 'auth.extra_info', 'intro_text', 'groups', 'database_connections_to_transact'];
|
||||
$asList = ['strategies.*', 'examples.models_source'];
|
||||
$differ = new ConfigDiffer(original: $this->docConfig->data, changed: $defaultConfig, ignorePaths: $ignore, asList: $asList);
|
||||
|
||||
$diff = $differ->getDiff();
|
||||
// Remove items the user has set
|
||||
$realDiff = [];
|
||||
foreach ($diff as $key => $value) {
|
||||
if (is_null($this->docConfig->get($key))) {
|
||||
$realDiff[$key] = $value;
|
||||
}
|
||||
}
|
||||
if (! empty($realDiff)) {
|
||||
$this->newLine();
|
||||
|
||||
$this->warn("You're using an updated version of Scribe, which may have added new items to the config file.");
|
||||
$this->info("Here's what is different:");
|
||||
foreach ($realDiff as $key => $item) {
|
||||
$this->line("{$key} --now defaults to-> {$item}");
|
||||
}
|
||||
|
||||
if (! $this->input->isInteractive()) {
|
||||
$this->info(sprintf('To upgrade, see the full changelog at: https://github.com/knuckleswtf/scribe/blob/%s/CHANGELOG.md,', Scribe::VERSION));
|
||||
$this->info('And config reference at https://scribe.knuckles.wtf/laravel/reference/config');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn('Check failed with error:');
|
||||
e::dumpExceptionIfVerbose($e);
|
||||
$this->warn('This did not affect your docs. Please report this issue in the project repo: https://github.com/knuckleswtf/scribe');
|
||||
}
|
||||
}
|
||||
|
||||
protected function sayGoodbye(bool $errored = false): void
|
||||
{
|
||||
$message = 'All done.';
|
||||
$url = null;
|
||||
|
||||
if ($this->docConfig->outputRoutedThroughLaravel()) {
|
||||
if ($this->docConfig->get('laravel.add_routes')) {
|
||||
$url = url($this->docConfig->get('laravel.docs_url'));
|
||||
}
|
||||
} elseif (Str::endsWith(base_path('public'), 'public') && Str::startsWith($this->docConfig->get('static.output_path'), 'public/')) {
|
||||
$url = url(str_replace('public/', '', $this->docConfig->get('static.output_path')));
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
if ($url) {
|
||||
$this->components->twoColumnDetail($message, $url);
|
||||
} else {
|
||||
$this->components->info($message);
|
||||
}
|
||||
|
||||
if ($errored) {
|
||||
$this->components->warn('Generated docs, but encountered some errors while processing routes.');
|
||||
$this->components->warn('Check the output above for details.');
|
||||
if (empty($_SERVER['SCRIBE_TESTS'])) {
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
vendor/knuckleswtf/scribe/src/Commands/MakeStrategy.php
vendored
Normal file
32
vendor/knuckleswtf/scribe/src/Commands/MakeStrategy.php
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Commands;
|
||||
|
||||
use Illuminate\Console\GeneratorCommand;
|
||||
|
||||
class MakeStrategy extends GeneratorCommand
|
||||
{
|
||||
protected $signature = 'scribe:strategy
|
||||
{name : Name of the class.}
|
||||
{--force : Overwrite file if it exists}
|
||||
';
|
||||
|
||||
protected $description = 'Create a new strategy class.';
|
||||
|
||||
protected $type = 'Strategy';
|
||||
|
||||
protected function getStub()
|
||||
{
|
||||
return __DIR__.'/stubs/strategy.stub';
|
||||
}
|
||||
|
||||
protected function getDefaultNamespace($rootNamespace)
|
||||
{
|
||||
return $rootNamespace.'\Docs\Strategies';
|
||||
}
|
||||
|
||||
protected function replaceClass($stub, $name)
|
||||
{
|
||||
return parent::replaceClass($stub, $name);
|
||||
}
|
||||
}
|
||||
182
vendor/knuckleswtf/scribe/src/Commands/Upgrade.php
vendored
Normal file
182
vendor/knuckleswtf/scribe/src/Commands/Upgrade.php
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Knuckles\Camel\Camel;
|
||||
use Knuckles\Scribe\GroupedEndpoints\GroupedEndpointsFactory;
|
||||
use Knuckles\Scribe\Scribe;
|
||||
use Knuckles\Scribe\Tools\PathConfig;
|
||||
use Shalvah\Upgrader\Upgrader;
|
||||
use Symfony\Component\VarExporter\VarExporter;
|
||||
|
||||
class Upgrade extends Command
|
||||
{
|
||||
protected $signature = 'scribe:upgrade {--dry-run : Print the changes that will be made, without actually making them}
|
||||
{--config=scribe : choose which config file to use}
|
||||
';
|
||||
|
||||
protected $description = '';
|
||||
|
||||
protected bool $applyChanges;
|
||||
|
||||
protected string $configName;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$this->applyChanges = ! $this->option('dry-run');
|
||||
|
||||
$this->configName = $this->option('config');
|
||||
if (! ($oldConfig = config($this->configName))) {
|
||||
$this->error("The specified config (config/{$this->configName}.php) doesn't exist.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (array_key_exists('interactive', $oldConfig)) {
|
||||
$this->error("This upgrade tool is for upgrading from Scribe v3 to v4, but it looks like you're coming from v2.");
|
||||
$this->error('Please install v3 and follow its upgrade guide first.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$isMajorUpgrade = array_key_exists('default_group', $oldConfig) || array_key_exists('faker_seed', $oldConfig);
|
||||
|
||||
if ($isMajorUpgrade) {
|
||||
$this->info('Welcome to the Scribe v3 to v4 upgrader.');
|
||||
}
|
||||
$this->line('Checking for config file changes...');
|
||||
|
||||
$upgrader = Upgrader::ofConfigFile("config/{$this->configName}.php", __DIR__.'/../../config/scribe.php')
|
||||
->dontTouch(
|
||||
'routes',
|
||||
'laravel.middleware',
|
||||
'postman.overrides',
|
||||
'openapi.overrides',
|
||||
'example_languages',
|
||||
'database_connections_to_transact',
|
||||
'strategies',
|
||||
'examples.models_source',
|
||||
'external.html_attributes'
|
||||
)
|
||||
->move('default_group', 'groups.default')
|
||||
->move('faker_seed', 'examples.faker_seed');
|
||||
|
||||
if (! $isMajorUpgrade) {
|
||||
$upgrader->dontTouch('groups');
|
||||
}
|
||||
|
||||
$changes = $upgrader->dryRun();
|
||||
if (empty($changes)) {
|
||||
$this->info('✔ No config file changes needed.');
|
||||
} else {
|
||||
$this->info('The following changes will be made to your config file:');
|
||||
$this->newLine();
|
||||
foreach ($changes as $change) {
|
||||
$this->line($change['description']);
|
||||
}
|
||||
|
||||
if ($this->applyChanges) {
|
||||
$upgrader->upgrade();
|
||||
$this->info("✔ Upgraded your config file. Your old config is backed up at config/{$this->configName}.php.bak.");
|
||||
}
|
||||
}
|
||||
$this->newLine();
|
||||
|
||||
if (! $isMajorUpgrade) {
|
||||
$this->info('✔ Done.');
|
||||
$this->info(sprintf('See the full changelog at https://github.com/knuckleswtf/scribe/blob/%s/CHANGELOG.md', Scribe::VERSION));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->finishV4Upgrade();
|
||||
}
|
||||
|
||||
protected function finishV4Upgrade(): void
|
||||
{
|
||||
$this->migrateToConfigFileSort();
|
||||
|
||||
if ($this->applyChanges) {
|
||||
if ($this->confirm('Do you have any custom strategies?')) {
|
||||
$this->line('1. Add a new property <info>public ?ExtractedEndpointData $endpointData;</info>.');
|
||||
$this->line('2. Replace the <info>array $routeRules</info> parameter in __invoke() with <info>array $routeRules = []</info> .');
|
||||
}
|
||||
$this->newLine();
|
||||
$this->info('✔ Done.');
|
||||
}
|
||||
|
||||
$this->line('See the release announcement at <href=https://scribe.knuckles.wtf/blog/laravel-v4>http://scribe.knuckles.wtf/blog/laravel-v4</> for the full upgrade guide!');
|
||||
}
|
||||
|
||||
/**
|
||||
* In v3, you sorted endpoints by reordering them in the group file, and groups by renaming the group files alphabetically
|
||||
* (or by using `beforeGroup`/`afterGroup` for custom endpoints).
|
||||
* v4 replaces them with the config item `groups.order`.
|
||||
*/
|
||||
protected function migrateToConfigFileSort()
|
||||
{
|
||||
$this->info('In v3, you sorted endpoints/groups by editing/renaming the generated YAML files (or `beforeGroup`/`afterGroup` for custom endpoints).');
|
||||
$this->info("We'll automatically import your current sorting into the config item `groups.order`.");
|
||||
|
||||
$defaultGroup = config($this->configName.'.default_group');
|
||||
$pathConfig = new PathConfig($this->configName);
|
||||
$extractedEndpoints = GroupedEndpointsFactory::fromCamelDir($pathConfig)->get();
|
||||
|
||||
$order = array_map(function (array $group) {
|
||||
return array_map(function (array $endpoint) {
|
||||
return $endpoint['metadata']['title'] ?: ($endpoint['httpMethods'][0].' /'.$endpoint['uri']);
|
||||
}, $group['endpoints']);
|
||||
}, $extractedEndpoints);
|
||||
$groupsOrder = array_keys($order);
|
||||
$keyIndices = array_flip($groupsOrder);
|
||||
|
||||
$userDefinedEndpoints = Camel::loadUserDefinedEndpoints(Camel::camelDir($pathConfig));
|
||||
|
||||
if ($userDefinedEndpoints) {
|
||||
foreach ($userDefinedEndpoints as $endpoint) {
|
||||
$groupName = $endpoint['metadata']['groupName'] ?? $defaultGroup;
|
||||
$endpointTitle = $endpoint['metadata']['title'] ?? ($endpoint['httpMethods'][0].' /'.$endpoint['uri']);
|
||||
|
||||
if (! isset($order[$groupName])) {
|
||||
// This is a new group; place it at the right spot.
|
||||
if ($nextGroup = $endpoint['metadata']['beforeGroup'] ?? null) {
|
||||
$index = $keyIndices[$nextGroup];
|
||||
array_splice($groupsOrder, $index, 0, [$groupName]);
|
||||
} elseif ($previousGroup = $endpoint['metadata']['afterGroup'] ?? null) {
|
||||
$index = $keyIndices[$previousGroup];
|
||||
array_splice($groupsOrder, $index + 1, 0, [$groupName]);
|
||||
} else {
|
||||
$groupsOrder[] = $groupName;
|
||||
}
|
||||
$order[$groupName] = [$endpointTitle];
|
||||
} else {
|
||||
// Existing group, add endpoint
|
||||
$order[$groupName] = [...$order[$groupName], $endpointTitle];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-add them, so it's sorted in the right order
|
||||
$newOrder = [];
|
||||
foreach ($groupsOrder as $groupName) {
|
||||
$newOrder[$groupName] = $order[$groupName];
|
||||
}
|
||||
|
||||
$output = VarExporter::export($newOrder);
|
||||
if ($this->applyChanges) {
|
||||
$configFile = "config/{$this->configName}.php";
|
||||
$output = str_replace("\n", "\n ", $output);
|
||||
$newContents = str_replace(
|
||||
"'order' => [],",
|
||||
"'order' => {$output},",
|
||||
file_get_contents($configFile)
|
||||
);
|
||||
file_put_contents($configFile, $newContents);
|
||||
$this->info('✔ Updated `groups.order`.');
|
||||
} else {
|
||||
$this->line('- `groups.order` will be set to:');
|
||||
$this->info($output);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
vendor/knuckleswtf/scribe/src/Commands/stubs/strategy.stub
vendored
Normal file
33
vendor/knuckleswtf/scribe/src/Commands/stubs/strategy.stub
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace DummyNamespace;
|
||||
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
|
||||
class DummyClass extends Strategy
|
||||
{
|
||||
/**
|
||||
* Trait containing some helper methods for dealing with "parameters",
|
||||
* such as generating examples and casting values to types.
|
||||
* Useful if your strategy extracts information about parameters or generates examples.
|
||||
*/
|
||||
use ParamHelpers;
|
||||
|
||||
/**
|
||||
* @link https://scribe.knuckles.wtf/laravel/advanced/plugins
|
||||
* @param ExtractedEndpointData $endpointData The endpoint we are currently processing.
|
||||
* Contains details about httpMethods, controller, method, route, url, etc, as well as already extracted data.
|
||||
* @param array $settings Settings to be applied to this strategy
|
||||
*
|
||||
* See the documentation linked above for more details about writing custom strategies.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $settings = []): ?array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
13
vendor/knuckleswtf/scribe/src/Config/AuthIn.php
vendored
Normal file
13
vendor/knuckleswtf/scribe/src/Config/AuthIn.php
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Config;
|
||||
|
||||
enum AuthIn: string
|
||||
{
|
||||
case BEARER = 'bearer';
|
||||
case BASIC = 'basic';
|
||||
case HEADER = 'header';
|
||||
case QUERY = 'query';
|
||||
case BODY = 'body';
|
||||
case QUERY_OR_BODY = 'query_or_body';
|
||||
}
|
||||
52
vendor/knuckleswtf/scribe/src/Config/Defaults.php
vendored
Normal file
52
vendor/knuckleswtf/scribe/src/Config/Defaults.php
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Config;
|
||||
|
||||
use Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
class Defaults
|
||||
{
|
||||
public const METADATA_STRATEGIES = [
|
||||
Strategies\Metadata\GetFromDocBlocks::class,
|
||||
Strategies\Metadata\GetFromMetadataAttributes::class,
|
||||
];
|
||||
|
||||
public const HEADERS_STRATEGIES = [
|
||||
Strategies\Headers\GetFromHeaderAttribute::class,
|
||||
Strategies\Headers\GetFromHeaderTag::class,
|
||||
];
|
||||
|
||||
public const URL_PARAMETERS_STRATEGIES = [
|
||||
Strategies\UrlParameters\GetFromLaravelAPI::class,
|
||||
Strategies\UrlParameters\GetFromUrlParamAttribute::class,
|
||||
Strategies\UrlParameters\GetFromUrlParamTag::class,
|
||||
];
|
||||
|
||||
public const QUERY_PARAMETERS_STRATEGIES = [
|
||||
Strategies\QueryParameters\GetFromFormRequest::class,
|
||||
Strategies\QueryParameters\GetFromInlineValidator::class,
|
||||
Strategies\QueryParameters\GetFromQueryParamAttribute::class,
|
||||
Strategies\QueryParameters\GetFromQueryParamTag::class,
|
||||
];
|
||||
|
||||
public const BODY_PARAMETERS_STRATEGIES = [
|
||||
Strategies\BodyParameters\GetFromFormRequest::class,
|
||||
Strategies\BodyParameters\GetFromInlineValidator::class,
|
||||
Strategies\BodyParameters\GetFromBodyParamAttribute::class,
|
||||
Strategies\BodyParameters\GetFromBodyParamTag::class,
|
||||
];
|
||||
|
||||
public const RESPONSES_STRATEGIES = [
|
||||
Strategies\Responses\UseResponseAttributes::class,
|
||||
Strategies\Responses\UseTransformerTags::class,
|
||||
Strategies\Responses\UseApiResourceTags::class,
|
||||
Strategies\Responses\UseResponseTag::class,
|
||||
Strategies\Responses\UseResponseFileTag::class,
|
||||
Strategies\Responses\ResponseCalls::class,
|
||||
];
|
||||
|
||||
public const RESPONSE_FIELDS_STRATEGIES = [
|
||||
Strategies\ResponseFields\GetFromResponseFieldAttribute::class,
|
||||
Strategies\ResponseFields\GetFromResponseFieldTag::class,
|
||||
];
|
||||
}
|
||||
58
vendor/knuckleswtf/scribe/src/Config/helpers.php
vendored
Normal file
58
vendor/knuckleswtf/scribe/src/Config/helpers.php
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Config;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
// Strategies can be:
|
||||
// 1. (Original) A class name, e.g. Strategies\Responses\ResponseCalls::class
|
||||
// 2. (New) A tuple containing the class name (or "static_data") as item 1, and its settings array as item 2
|
||||
|
||||
/**
|
||||
* Remove one or more strategies from a list of strategies.
|
||||
*/
|
||||
function removeStrategies(array $strategiesList, array $strategyNamesToRemove): array
|
||||
{
|
||||
$correspondingStrategies = Arr::where($strategiesList, function ($strategy) use ($strategyNamesToRemove) {
|
||||
$strategyName = is_string($strategy) ? $strategy : $strategy[0];
|
||||
|
||||
return in_array($strategyName, $strategyNamesToRemove);
|
||||
});
|
||||
|
||||
foreach ($correspondingStrategies as $key => $value) {
|
||||
unset($strategiesList[$key]);
|
||||
}
|
||||
|
||||
return $strategiesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/replace a strategy and its settings in a list of strategies.
|
||||
* This method generates a tuple containing [strategyName, settingsArray],
|
||||
* and adds or replaces the strategy entry in the list.
|
||||
*
|
||||
* @param array $configurationTuple Tuple of [strategyName, settingsArray].
|
||||
* By default, all strategies support the "only" and "except" setting to apply them to specific endpoints.
|
||||
* You can easily create the tuple by calling Strategy::wrapWithSettings(only: [], except: []).
|
||||
*/
|
||||
function configureStrategy(array $strategiesList, array $configurationTuple): array
|
||||
{
|
||||
$strategyFound = false;
|
||||
$strategiesList = array_map(function ($strategy) use ($configurationTuple, &$strategyFound) {
|
||||
$strategyName = is_string($strategy) ? $strategy : $strategy[0];
|
||||
if ($strategyName === $configurationTuple[0]) {
|
||||
$strategyFound = true;
|
||||
|
||||
return $configurationTuple;
|
||||
}
|
||||
|
||||
return $strategy;
|
||||
}, $strategiesList);
|
||||
|
||||
// If strategy wasn't in there, add it.
|
||||
if (! $strategyFound) {
|
||||
$strategiesList = array_merge($strategiesList, [$configurationTuple]);
|
||||
}
|
||||
|
||||
return $strategiesList;
|
||||
}
|
||||
11
vendor/knuckleswtf/scribe/src/Exceptions/CouldntFindFactory.php
vendored
Normal file
11
vendor/knuckleswtf/scribe/src/Exceptions/CouldntFindFactory.php
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Exceptions;
|
||||
|
||||
class CouldntFindFactory extends \RuntimeException implements ScribeException
|
||||
{
|
||||
public static function forModel(string $modelName): self
|
||||
{
|
||||
return new self("Couldn't find the Eloquent model factory. Did you add the HasFactory trait to your {$modelName} model?");
|
||||
}
|
||||
}
|
||||
11
vendor/knuckleswtf/scribe/src/Exceptions/CouldntGetRouteDetails.php
vendored
Normal file
11
vendor/knuckleswtf/scribe/src/Exceptions/CouldntGetRouteDetails.php
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Exceptions;
|
||||
|
||||
class CouldntGetRouteDetails extends \RuntimeException implements ScribeException
|
||||
{
|
||||
public static function new(): self
|
||||
{
|
||||
return new self('Unable to retrieve controller and method for route; try running `php artisan route:clear`');
|
||||
}
|
||||
}
|
||||
15
vendor/knuckleswtf/scribe/src/Exceptions/CouldntProcessValidationRule.php
vendored
Normal file
15
vendor/knuckleswtf/scribe/src/Exceptions/CouldntProcessValidationRule.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Exceptions;
|
||||
|
||||
class CouldntProcessValidationRule extends \RuntimeException implements ScribeException
|
||||
{
|
||||
public static function forParam(string $paramName, $rule, \Throwable $innerException): self
|
||||
{
|
||||
return new self(
|
||||
"Couldn't process the validation rule ".var_export($rule, true)." for the param `{$paramName}`: {$innerException->getMessage()}",
|
||||
0,
|
||||
$innerException
|
||||
);
|
||||
}
|
||||
}
|
||||
16
vendor/knuckleswtf/scribe/src/Exceptions/CouldntStartDatabaseTransaction.php
vendored
Normal file
16
vendor/knuckleswtf/scribe/src/Exceptions/CouldntStartDatabaseTransaction.php
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Exceptions;
|
||||
|
||||
class CouldntStartDatabaseTransaction extends \RuntimeException implements ScribeException
|
||||
{
|
||||
public static function forConnection(string $connectionName, \Throwable $originalException): self
|
||||
{
|
||||
return new self(
|
||||
"Couldn't start a database transaction for the connection {$connectionName}. "
|
||||
."Make sure this database is running. If you aren't using this connection, remove it from your `databaseConnectionsToTransact` config",
|
||||
0,
|
||||
$originalException
|
||||
);
|
||||
}
|
||||
}
|
||||
15
vendor/knuckleswtf/scribe/src/Exceptions/DatabaseTransactionsNotSupported.php
vendored
Normal file
15
vendor/knuckleswtf/scribe/src/Exceptions/DatabaseTransactionsNotSupported.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Exceptions;
|
||||
|
||||
class DatabaseTransactionsNotSupported extends \RuntimeException implements ScribeException
|
||||
{
|
||||
public static function create(string $connectionName, string $driverName)
|
||||
{
|
||||
return new self(
|
||||
"Database driver [{$driverName}] for connection [{$connectionName}] does not support transactions."
|
||||
." To allow Scribe to proceed, remove \"{$connectionName}\" from the \"database_connections_to_transact\" config array."
|
||||
.' Note that any changes to your database will be persisted.'
|
||||
);
|
||||
}
|
||||
}
|
||||
16
vendor/knuckleswtf/scribe/src/Exceptions/GroupNotFound.php
vendored
Normal file
16
vendor/knuckleswtf/scribe/src/Exceptions/GroupNotFound.php
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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
|
||||
);
|
||||
}
|
||||
}
|
||||
15
vendor/knuckleswtf/scribe/src/Exceptions/ProblemParsingValidationRules.php
vendored
Normal file
15
vendor/knuckleswtf/scribe/src/Exceptions/ProblemParsingValidationRules.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Exceptions;
|
||||
|
||||
class ProblemParsingValidationRules extends \RuntimeException implements ScribeException
|
||||
{
|
||||
public static function forParam(string $paramName, \Throwable $innerException): self
|
||||
{
|
||||
return new self(
|
||||
"Problem processing validation rules for the param `{$paramName}`: {$innerException->getMessage()}",
|
||||
0,
|
||||
$innerException
|
||||
);
|
||||
}
|
||||
}
|
||||
9
vendor/knuckleswtf/scribe/src/Exceptions/ScribeException.php
vendored
Normal file
9
vendor/knuckleswtf/scribe/src/Exceptions/ScribeException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
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 {}
|
||||
169
vendor/knuckleswtf/scribe/src/Extracting/ApiDetails.php
vendored
Normal file
169
vendor/knuckleswtf/scribe/src/Extracting/ApiDetails.php
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\DocumentationConfig;
|
||||
use Knuckles\Scribe\Tools\PathConfig;
|
||||
use Knuckles\Scribe\Tools\Utils as u;
|
||||
|
||||
/**
|
||||
* Handles extracting other API details — intro, auth.
|
||||
*/
|
||||
class ApiDetails
|
||||
{
|
||||
private DocumentationConfig $config;
|
||||
|
||||
private string $baseUrl;
|
||||
|
||||
private bool $preserveUserChanges;
|
||||
|
||||
private string $markdownOutputPath;
|
||||
|
||||
private string $fileHashesTrackingFile;
|
||||
|
||||
private array $lastKnownFileContentHashes = [];
|
||||
|
||||
public function __construct(
|
||||
PathConfig $paths,
|
||||
?DocumentationConfig $config = null,
|
||||
bool $preserveUserChanges = true,
|
||||
) {
|
||||
$this->markdownOutputPath = $paths->intermediateOutputPath(); // .scribe by default
|
||||
// If no config is injected, pull from global. Makes testing easier.
|
||||
$this->config = $config ?: new DocumentationConfig(config($paths->configName));
|
||||
$this->baseUrl = $this->config->get('base_url') ?? config('app.url');
|
||||
$this->preserveUserChanges = $preserveUserChanges;
|
||||
|
||||
$this->fileHashesTrackingFile = $this->markdownOutputPath.'/.filehashes';
|
||||
$this->lastKnownFileContentHashes = [];
|
||||
}
|
||||
|
||||
public function writeMarkdownFiles(): void
|
||||
{
|
||||
c::task(
|
||||
'Extracting intro and auth Markdown files to: '.$this->markdownOutputPath,
|
||||
function () {
|
||||
if (! is_dir($this->markdownOutputPath)) {
|
||||
mkdir($this->markdownOutputPath, 0o777, true);
|
||||
}
|
||||
|
||||
$this->fetchFileHashesFromTrackingFile();
|
||||
|
||||
$this->writeIntroMarkdownFile();
|
||||
$this->writeAuthMarkdownFile();
|
||||
|
||||
$this->writeContentsTrackingFile();
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function writeIntroMarkdownFile(): void
|
||||
{
|
||||
$introMarkdownFile = $this->markdownOutputPath.'/intro.md';
|
||||
if ($this->hasFileBeenModified($introMarkdownFile)) {
|
||||
if ($this->preserveUserChanges) {
|
||||
c::warn("Skipping modified file {$introMarkdownFile}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
c::warn("Discarding manual changes for file {$introMarkdownFile} because you specified --force");
|
||||
}
|
||||
|
||||
$introMarkdown = view('scribe::markdown.intro')
|
||||
->with('description', $this->config->get('description', ''))
|
||||
->with('introText', $this->config->get('intro_text', ''))
|
||||
->with('baseUrl', $this->baseUrl)->render();
|
||||
$this->writeMarkdownFileAndRecordHash($introMarkdownFile, $introMarkdown);
|
||||
}
|
||||
|
||||
public function writeAuthMarkdownFile(): void
|
||||
{
|
||||
$authMarkdownFile = $this->markdownOutputPath.'/auth.md';
|
||||
if ($this->hasFileBeenModified($authMarkdownFile)) {
|
||||
if ($this->preserveUserChanges) {
|
||||
c::warn("Skipping modified file {$authMarkdownFile}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
c::warn("Discarding manual changes for file {$authMarkdownFile} because you specified --force");
|
||||
}
|
||||
|
||||
$isAuthed = $this->config->get('auth.enabled', false);
|
||||
$authDescription = '';
|
||||
$extraInfo = '';
|
||||
|
||||
if ($isAuthed) {
|
||||
$strategy = $this->config->get('auth.in');
|
||||
$parameterName = $this->config->get('auth.name');
|
||||
$authDescription = u::trans(
|
||||
"scribe::auth.instruction.{$strategy}",
|
||||
[
|
||||
'parameterName' => $parameterName,
|
||||
'placeholder' => $this->config->get('auth.placeholder') ?: 'your-token']
|
||||
);
|
||||
$authDescription .= "\n\n".u::trans('scribe::auth.details');
|
||||
$extraInfo = $this->config->get('auth.extra_info', '');
|
||||
}
|
||||
|
||||
$authMarkdown = view('scribe::markdown.auth', [
|
||||
'isAuthed' => $isAuthed,
|
||||
'authDescription' => $authDescription,
|
||||
'extraAuthInfo' => $extraInfo,
|
||||
])->render();
|
||||
$this->writeMarkdownFileAndRecordHash($authMarkdownFile, $authMarkdown);
|
||||
}
|
||||
|
||||
protected function writeMarkdownFileAndRecordHash(string $filePath, string $markdown): void
|
||||
{
|
||||
file_put_contents($filePath, $markdown);
|
||||
$this->lastKnownFileContentHashes[$filePath] = hash_file('md5', $filePath);
|
||||
}
|
||||
|
||||
protected function writeContentsTrackingFile(): void
|
||||
{
|
||||
$content = "# GENERATED. YOU SHOULDN'T MODIFY OR DELETE THIS FILE.\n";
|
||||
$content .= "# Scribe uses this file to know when you change something manually in your docs.\n";
|
||||
$content .= collect($this->lastKnownFileContentHashes)
|
||||
->map(fn ($hash, $filePath) => "{$filePath}={$hash}")->implode("\n");
|
||||
file_put_contents($this->fileHashesTrackingFile, $content);
|
||||
}
|
||||
|
||||
protected function hasFileBeenModified(string $filePath): bool
|
||||
{
|
||||
if (! file_exists($filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldFileHash = $this->lastKnownFileContentHashes[$filePath] ?? null;
|
||||
|
||||
if ($oldFileHash) {
|
||||
$currentFileHash = hash_file('md5', $filePath);
|
||||
|
||||
// No danger of a timing attack, so no need for hash_equals() comparison
|
||||
return $currentFileHash !== $oldFileHash;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function fetchFileHashesFromTrackingFile()
|
||||
{
|
||||
if (file_exists($this->fileHashesTrackingFile)) {
|
||||
$lastKnownFileHashes = explode("\n", mb_trim(file_get_contents($this->fileHashesTrackingFile)));
|
||||
// First two lines are comments
|
||||
array_shift($lastKnownFileHashes);
|
||||
array_shift($lastKnownFileHashes);
|
||||
$this->lastKnownFileContentHashes = collect($lastKnownFileHashes)
|
||||
->mapWithKeys(function ($line) {
|
||||
[$filePath, $hash] = explode('=', $line);
|
||||
|
||||
return [$filePath => $hash];
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
69
vendor/knuckleswtf/scribe/src/Extracting/DatabaseTransactionHelpers.php
vendored
Normal file
69
vendor/knuckleswtf/scribe/src/Extracting/DatabaseTransactionHelpers.php
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use Knuckles\Scribe\Exceptions\CouldntStartDatabaseTransaction;
|
||||
use Knuckles\Scribe\Exceptions\DatabaseTransactionsNotSupported;
|
||||
use Knuckles\Scribe\Tools\DocumentationConfig;
|
||||
|
||||
trait DatabaseTransactionHelpers
|
||||
{
|
||||
/**
|
||||
* Returns an instance of the documentation config.
|
||||
*/
|
||||
abstract public function getConfig(): DocumentationConfig;
|
||||
|
||||
private function connectionsToTransact()
|
||||
{
|
||||
return $this->getConfig()->get('database_connections_to_transact', []);
|
||||
}
|
||||
|
||||
private function startDbTransaction()
|
||||
{
|
||||
foreach ($this->connectionsToTransact() as $connection) {
|
||||
$database ??= app('db');
|
||||
|
||||
$driver = $database->connection($connection);
|
||||
|
||||
if (self::driverSupportsTransactions($driver)) {
|
||||
try {
|
||||
$driver->beginTransaction();
|
||||
} catch (\Throwable $e) {
|
||||
throw CouldntStartDatabaseTransaction::forConnection($connection, $e);
|
||||
}
|
||||
} else {
|
||||
$driverClassName = get_class($driver);
|
||||
|
||||
throw DatabaseTransactionsNotSupported::create($connection, $driverClassName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function endDbTransaction()
|
||||
{
|
||||
foreach ($this->connectionsToTransact() as $connection) {
|
||||
$database ??= app('db');
|
||||
|
||||
$driver = $database->connection($connection);
|
||||
|
||||
try {
|
||||
$driver->rollback();
|
||||
} catch (\Exception $e) {
|
||||
// Any error handling should have been done on the startDbTransaction() side
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function driverSupportsTransactions($driver): bool
|
||||
{
|
||||
$methods = ['beginTransaction', 'rollback'];
|
||||
|
||||
foreach ($methods as $method) {
|
||||
if (! method_exists($driver, $method)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
520
vendor/knuckleswtf/scribe/src/Extracting/Extractor.php
vendored
Normal file
520
vendor/knuckleswtf/scribe/src/Extracting/Extractor.php
vendored
Normal file
@@ -0,0 +1,520 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use Faker\Factory;
|
||||
use Illuminate\Http\Testing\File;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Camel\Extraction\Metadata;
|
||||
use Knuckles\Camel\Extraction\Parameter;
|
||||
use Knuckles\Camel\Extraction\ResponseCollection;
|
||||
use Knuckles\Camel\Extraction\ResponseField;
|
||||
use Knuckles\Camel\Output\OutputEndpointData;
|
||||
use Knuckles\Scribe\Extracting\Strategies\StaticData;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\DocumentationConfig;
|
||||
use Knuckles\Scribe\Tools\RoutePatternMatcher;
|
||||
|
||||
class Extractor
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
private DocumentationConfig $config;
|
||||
|
||||
private static ?Route $routeBeingProcessed = null;
|
||||
|
||||
public function __construct(?DocumentationConfig $config = null)
|
||||
{
|
||||
// If no config is injected, pull from global
|
||||
$this->config = $config ?: new DocumentationConfig(config('scribe'));
|
||||
}
|
||||
|
||||
/**
|
||||
* External interface that allows users to know what route is currently being processed.
|
||||
*/
|
||||
public static function getRouteBeingProcessed(): ?Route
|
||||
{
|
||||
return self::$routeBeingProcessed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $routeRules Rules to apply when generating documentation for this route. Deprecated. Use strategy config instead.
|
||||
*/
|
||||
public function processRoute(Route $route, array $routeRules = []): ExtractedEndpointData
|
||||
{
|
||||
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
|
||||
$endpointData->headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
// We need to do all this so response calls can work correctly,
|
||||
// even though they're only needed for output
|
||||
// Note that this
|
||||
[$files, $regularParameters] = OutputEndpointData::splitIntoFileAndRegularParameters($endpointData->cleanBodyParameters);
|
||||
if (count($files)) {
|
||||
$endpointData->headers['Content-Type'] = 'multipart/form-data';
|
||||
}
|
||||
$endpointData->fileParameters = $files;
|
||||
$endpointData->cleanBodyParameters = $regularParameters;
|
||||
|
||||
$this->fetchResponses($endpointData, $routeRules);
|
||||
$this->mergeInheritedMethodsData('responses', $endpointData, $inheritedDocsOverrides);
|
||||
|
||||
$this->fetchResponseFields($endpointData, $routeRules);
|
||||
$this->mergeInheritedMethodsData('responseFields', $endpointData, $inheritedDocsOverrides);
|
||||
|
||||
self::$routeBeingProcessed = null;
|
||||
|
||||
return $endpointData;
|
||||
}
|
||||
|
||||
public function shouldSkipRoute($route, array $routesToExclude, array $routesToInclude): bool
|
||||
{
|
||||
if (! empty($routesToExclude) && RoutePatternMatcher::matches($route, $routesToExclude)) {
|
||||
return true;
|
||||
}
|
||||
if (! empty($routesToInclude) && ! RoutePatternMatcher::matches($route, $routesToInclude)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method prepares and simplifies request parameters for use in example requests and response calls.
|
||||
* It takes in an array with rich details about a parameter eg
|
||||
* ['age' => new Parameter([
|
||||
* 'description' => 'The age',
|
||||
* 'example' => 12,
|
||||
* 'required' => false,
|
||||
* ])]
|
||||
* And transforms them into key-example pairs : ['age' => 12]
|
||||
* It also filters out parameters which have null values and have 'required' as false.
|
||||
* It converts all file params that have string examples to actual files (instances of UploadedFile).
|
||||
*
|
||||
* @param array<string,Parameter> $parameters
|
||||
*/
|
||||
public static function cleanParams(array $parameters): array
|
||||
{
|
||||
$cleanParameters = [];
|
||||
|
||||
/**
|
||||
* @var string $paramName
|
||||
* @var Parameter $details
|
||||
*/
|
||||
foreach ($parameters as $paramName => $details) {
|
||||
// Remove params which have no intentional examples and are optional.
|
||||
if (! $details->exampleWasSpecified) {
|
||||
if (is_null($details->example) && $details->required === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($details->type === 'file') {
|
||||
if (is_string($details->example)) {
|
||||
$details->example = self::convertStringValueToUploadedFileInstance($details->example);
|
||||
} elseif (is_null($details->example)) {
|
||||
$details->example = (new self)->generateDummyValue($details->type);
|
||||
}
|
||||
}
|
||||
|
||||
if (Str::startsWith($paramName, '[].')) { // Entire body is an array
|
||||
if (empty($parameters['[]'])) { // Make sure there's a parent
|
||||
$cleanParameters['[]'] = [[], []];
|
||||
$parameters['[]'] = new Parameter([
|
||||
'name' => '[]',
|
||||
'type' => 'object[]',
|
||||
'description' => '',
|
||||
'required' => true,
|
||||
'example' => [$paramName => $details->example],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (Str::contains($paramName, '.')) { // Object field (or array of objects)
|
||||
self::setObject($cleanParameters, $paramName, $details->example, $parameters, $details->required);
|
||||
} else {
|
||||
$cleanParameters[$paramName] = $details->example;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, if the body is an array, flatten it.
|
||||
if (isset($cleanParameters['[]'])) {
|
||||
$cleanParameters = $cleanParameters['[]'];
|
||||
}
|
||||
|
||||
return $cleanParameters;
|
||||
}
|
||||
|
||||
public static function setObject(array &$results, string $path, $value, array $source, bool $isRequired)
|
||||
{
|
||||
$parts = explode('.', $path);
|
||||
|
||||
array_pop($parts); // Get rid of the field name
|
||||
|
||||
$baseName = implode('.', $parts);
|
||||
// For array fields, the type should be indicated in the source object by now;
|
||||
// eg test.items[] would actually be described as name: test.items, type: object[]
|
||||
// So we get rid of that ending []
|
||||
// For other fields (eg test.items[].name), it remains as-is
|
||||
$baseNameInOriginalParams = $baseName;
|
||||
while (Str::endsWith($baseNameInOriginalParams, '[]')) {
|
||||
$baseNameInOriginalParams = mb_substr($baseNameInOriginalParams, 0, -2);
|
||||
}
|
||||
// When the body is an array, param names will be "[].paramname",
|
||||
// so $baseNameInOriginalParams here will be empty
|
||||
if (Str::startsWith($path, '[].')) {
|
||||
$baseNameInOriginalParams = '[]';
|
||||
}
|
||||
|
||||
if (Arr::has($source, $baseNameInOriginalParams)) {
|
||||
/** @var Parameter $parentData */
|
||||
$parentData = Arr::get($source, $baseNameInOriginalParams);
|
||||
// Path we use for data_set
|
||||
$dotPath = str_replace('[]', '.0', $path);
|
||||
|
||||
// Don't overwrite parent if there's already data there
|
||||
|
||||
if ($parentData->type === 'object') {
|
||||
$parentPath = explode('.', $dotPath);
|
||||
$property = array_pop($parentPath);
|
||||
$parentPath = implode('.', $parentPath);
|
||||
|
||||
$exampleFromParent = Arr::get($results, $dotPath) ?? $parentData->example[$property] ?? null;
|
||||
if (empty($exampleFromParent)) {
|
||||
Arr::set($results, $dotPath, $value);
|
||||
}
|
||||
} elseif ($parentData->type === 'object[]') {
|
||||
// When the body is an array, param names will be "[].paramname", so dot paths won't work correctly with "[]"
|
||||
if (Str::startsWith($path, '[].')) {
|
||||
$valueDotPath = mb_substr($dotPath, 3); // Remove initial '.0.'
|
||||
if (isset($results['[]'][0]) && ! Arr::has($results['[]'][0], $valueDotPath)) {
|
||||
Arr::set($results['[]'][0], $valueDotPath, $value);
|
||||
}
|
||||
} else {
|
||||
$parentPath = explode('.', $dotPath);
|
||||
$index = (int) array_pop($parentPath);
|
||||
$parentPath = implode('.', $parentPath);
|
||||
|
||||
$exampleFromParent = Arr::get($results, $dotPath) ?? $parentData->example[$index] ?? null;
|
||||
if (empty($exampleFromParent)) {
|
||||
Arr::set($results, $dotPath, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function addAuthField(ExtractedEndpointData $endpointData): void
|
||||
{
|
||||
$isApiAuthed = $this->config->get('auth.enabled', false);
|
||||
if (! $isApiAuthed || ! $endpointData->metadata->authenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
$strategy = $this->config->get('auth.in');
|
||||
$parameterName = $this->config->get('auth.name');
|
||||
|
||||
$faker = Factory::create();
|
||||
if ($seed = $this->config->get('examples.faker_seed')) {
|
||||
$faker->seed($seed);
|
||||
}
|
||||
$token = $faker->shuffleString('abcdefghkvaZVDPE1864563');
|
||||
$valueToUse = $this->config->get('auth.use_value');
|
||||
$valueToDisplay = $this->config->get('auth.placeholder');
|
||||
|
||||
switch ($strategy) {
|
||||
case 'query':
|
||||
case 'query_or_body':
|
||||
$endpointData->auth = ['queryParameters', $parameterName, $valueToUse ?: $token];
|
||||
$endpointData->queryParameters[$parameterName] = new Parameter([
|
||||
'name' => $parameterName,
|
||||
'type' => 'string',
|
||||
'example' => $valueToDisplay ?: $token,
|
||||
'description' => 'Authentication key.',
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
return;
|
||||
|
||||
case 'body':
|
||||
$endpointData->auth = ['bodyParameters', $parameterName, $valueToUse ?: $token];
|
||||
$endpointData->bodyParameters[$parameterName] = new Parameter([
|
||||
'name' => $parameterName,
|
||||
'type' => 'string',
|
||||
'example' => $valueToDisplay ?: $token,
|
||||
'description' => 'Authentication key.',
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
return;
|
||||
|
||||
case 'bearer':
|
||||
$endpointData->auth = ['headers', 'Authorization', 'Bearer '.($valueToUse ?: $token)];
|
||||
$endpointData->headers['Authorization'] = 'Bearer '.($valueToDisplay ?: $token);
|
||||
|
||||
return;
|
||||
|
||||
case 'basic':
|
||||
$endpointData->auth = ['headers', 'Authorization', 'Basic '.($valueToUse ?: base64_encode($token))];
|
||||
$endpointData->headers['Authorization'] = 'Basic '.($valueToDisplay ?: base64_encode($token));
|
||||
|
||||
return;
|
||||
|
||||
case 'header':
|
||||
$endpointData->auth = ['headers', $parameterName, $valueToUse ?: $token];
|
||||
$endpointData->headers[$parameterName] = $valueToDisplay ?: $token;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static function transformOldRouteRulesIntoNewSettings($stage, $rulesToApply, $strategyName, $strategySettings = [])
|
||||
{
|
||||
if ($stage === 'responses' && Str::is('*ResponseCalls*', $strategyName) && ! empty($rulesToApply['response_calls'])) {
|
||||
// Transform `methods` to `only`
|
||||
$strategySettings = [];
|
||||
|
||||
if (isset($rulesToApply['response_calls']['methods'])) {
|
||||
if (empty($rulesToApply['response_calls']['methods'])) {
|
||||
// This means all routes are forbidden
|
||||
$strategySettings['except'] = ['*'];
|
||||
} else {
|
||||
$strategySettings['only'] = array_map(
|
||||
fn ($method) => "{$method} *",
|
||||
$rulesToApply['response_calls']['methods']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all other keys over as is
|
||||
foreach (array_keys($rulesToApply['response_calls']) as $key) {
|
||||
if ($key === 'methods') {
|
||||
continue;
|
||||
}
|
||||
$strategySettings[$key] = $rulesToApply['response_calls'][$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $strategySettings;
|
||||
}
|
||||
|
||||
protected function fetchMetadata(ExtractedEndpointData $endpointData, array $rulesToApply): void
|
||||
{
|
||||
$endpointData->metadata = new Metadata([
|
||||
'groupName' => $this->config->get('groups.default', ''),
|
||||
'authenticated' => $this->config->get('auth.default', false),
|
||||
]);
|
||||
|
||||
$this->iterateThroughStrategies('metadata', $endpointData, $rulesToApply, function ($results) use ($endpointData) {
|
||||
foreach ($results as $key => $item) {
|
||||
$hadPreviousValue = ! is_null($endpointData->metadata->{$key});
|
||||
$noNewValueSet = is_null($item) || $item === '';
|
||||
if ($hadPreviousValue && $noNewValueSet) {
|
||||
continue;
|
||||
}
|
||||
$endpointData->metadata->{$key} = $item;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function fetchUrlParameters(ExtractedEndpointData $endpointData, array $rulesToApply): void
|
||||
{
|
||||
$this->iterateThroughStrategies('urlParameters', $endpointData, $rulesToApply, function ($results) use ($endpointData) {
|
||||
foreach ($results as $key => $item) {
|
||||
if (empty($item['name'])) {
|
||||
$item['name'] = $key;
|
||||
}
|
||||
$endpointData->urlParameters[$key] = Parameter::create($item, $endpointData->urlParameters[$key] ?? []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function fetchQueryParameters(ExtractedEndpointData $endpointData, array $rulesToApply): void
|
||||
{
|
||||
$this->iterateThroughStrategies('queryParameters', $endpointData, $rulesToApply, function ($results) use ($endpointData) {
|
||||
foreach ($results as $key => $item) {
|
||||
if (empty($item['name'])) {
|
||||
$item['name'] = $key;
|
||||
}
|
||||
$endpointData->queryParameters[$key] = Parameter::create($item, $endpointData->queryParameters[$key] ?? []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function fetchBodyParameters(ExtractedEndpointData $endpointData, array $rulesToApply): void
|
||||
{
|
||||
$this->iterateThroughStrategies('bodyParameters', $endpointData, $rulesToApply, function ($results) use ($endpointData) {
|
||||
foreach ($results as $key => $item) {
|
||||
if (empty($item['name'])) {
|
||||
$item['name'] = $key;
|
||||
}
|
||||
$endpointData->bodyParameters[$key] = Parameter::create($item, $endpointData->bodyParameters[$key] ?? []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function fetchResponses(ExtractedEndpointData $endpointData, array $rulesToApply): void
|
||||
{
|
||||
$this->iterateThroughStrategies('responses', $endpointData, $rulesToApply, function ($results) use ($endpointData) {
|
||||
// Responses from different strategies are all added, not overwritten
|
||||
$endpointData->responses->concat($results);
|
||||
});
|
||||
// Ensure 200 responses come first
|
||||
$endpointData->responses = new ResponseCollection($endpointData->responses->sortBy('status')->values());
|
||||
}
|
||||
|
||||
protected function fetchResponseFields(ExtractedEndpointData $endpointData, array $rulesToApply): void
|
||||
{
|
||||
$this->iterateThroughStrategies('responseFields', $endpointData, $rulesToApply, function ($results) use ($endpointData) {
|
||||
foreach ($results as $key => $item) {
|
||||
$endpointData->responseFields[$key] = Parameter::create($item, $endpointData->responseFields[$key] ?? []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function fetchRequestHeaders(ExtractedEndpointData $endpointData, array $rulesToApply): void
|
||||
{
|
||||
$this->iterateThroughStrategies('headers', $endpointData, $rulesToApply, function ($results) use ($endpointData) {
|
||||
foreach ($results as $key => $item) {
|
||||
if ($item) {
|
||||
$endpointData->headers[$key] = $item;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through all defined strategies for this stage.
|
||||
* A strategy may return an array of attributes
|
||||
* to be added to that stage data, or it may modify the stage data directly.
|
||||
*
|
||||
* @param array $rulesToApply Deprecated. Use strategy config instead.
|
||||
* @param callable $handler function to run after each strategy returns its results (an array)
|
||||
*/
|
||||
protected function iterateThroughStrategies(
|
||||
string $stage,
|
||||
ExtractedEndpointData $endpointData,
|
||||
array $rulesToApply,
|
||||
callable $handler,
|
||||
): void {
|
||||
$strategies = $this->config->get("strategies.{$stage}", []);
|
||||
|
||||
foreach ($strategies as $strategyClassOrTuple) {
|
||||
if (is_array($strategyClassOrTuple)) {
|
||||
[$strategyClass, &$settings] = $strategyClassOrTuple;
|
||||
if ($strategyClass === 'override') {
|
||||
c::warn("The 'override' strategy was renamed to 'static_data', and will stop working in the future. Please replace 'override' in your config file with 'static_data'.");
|
||||
$strategyClass = 'static_data';
|
||||
}
|
||||
if ($strategyClass === 'static_data') {
|
||||
$strategyClass = StaticData::class;
|
||||
// Static data can be short: ['static_data', ['key' => 'value']],
|
||||
// or extended ['static_data', ['data' => ['key' => 'value'], 'only' => ['GET *'], 'except' => []]],
|
||||
$settingsFormat = array_key_exists('data', $settings) ? 'extended' : 'short';
|
||||
$settings = match ($settingsFormat) {
|
||||
'extended' => $settings,
|
||||
'short' => ['data' => $settings],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
$strategyClass = $strategyClassOrTuple;
|
||||
$settings = [];
|
||||
}
|
||||
|
||||
$routesToExclude = Arr::wrap($settings['except'] ?? []);
|
||||
$routesToInclude = Arr::wrap($settings['only'] ?? []);
|
||||
|
||||
if ($this->shouldSkipRoute($endpointData->route, $routesToExclude, $routesToInclude)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$strategy = new $strategyClass($this->config);
|
||||
$results = $strategy($endpointData, $settings);
|
||||
if (is_array($results)) {
|
||||
$handler($results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static function convertStringValueToUploadedFileInstance(string $filePath): UploadedFile
|
||||
{
|
||||
$fileName = basename($filePath);
|
||||
|
||||
return new File($fileName, fopen($filePath, 'r'));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
} elseif (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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
36
vendor/knuckleswtf/scribe/src/Extracting/FindsFormRequestForMethod.php
vendored
Normal file
36
vendor/knuckleswtf/scribe/src/Extracting/FindsFormRequestForMethod.php
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
trait FindsFormRequestForMethod
|
||||
{
|
||||
protected function getFormRequestReflectionClass(\ReflectionFunctionAbstract $method): ?\ReflectionClass
|
||||
{
|
||||
foreach ($method->getParameters() as $argument) {
|
||||
$argType = $argument->getType();
|
||||
if ($argType === null || $argType instanceof \ReflectionUnionType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$argumentClassName = $argType->getName();
|
||||
|
||||
if (! class_exists($argumentClassName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$argumentClass = new \ReflectionClass($argumentClassName);
|
||||
} catch (\ReflectionException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($argumentClass->isSubclassOf(FormRequest::class)) {
|
||||
return $argumentClass;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
109
vendor/knuckleswtf/scribe/src/Extracting/InstantiatesExampleModels.php
vendored
Normal file
109
vendor/knuckleswtf/scribe/src/Extracting/InstantiatesExampleModels.php
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\ErrorHandlingUtils as e;
|
||||
use Knuckles\Scribe\Tools\Utils;
|
||||
|
||||
trait InstantiatesExampleModels
|
||||
{
|
||||
/**
|
||||
* @param string[] $factoryStates
|
||||
* @param string[] $relations
|
||||
* @param null|\ReflectionFunctionAbstract $transformationMethod A method which has the model as its first parameter. Useful if the `$type` is empty.
|
||||
* @return null|Model|object
|
||||
*/
|
||||
protected function instantiateExampleModel(
|
||||
?string $type = null,
|
||||
array $factoryStates = [],
|
||||
array $relations = [],
|
||||
?\ReflectionFunctionAbstract $transformationMethod = null,
|
||||
array $withCount = [],
|
||||
) {
|
||||
// If the API Resource uses an empty resource, there won't be an example model
|
||||
if ($type === null && $transformationMethod === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type === null) {
|
||||
$parameter = Arr::first($transformationMethod->getParameters());
|
||||
$parameterType = $parameter->hasType() ? $parameter->getType() : null;
|
||||
if ($parameterType instanceof \ReflectionNamedType
|
||||
&& ! $parameterType->isBuiltin() && class_exists($parameterType->getName())) {
|
||||
// Ladies and gentlemen, we have a type!
|
||||
$type = $parameterType->getName();
|
||||
}
|
||||
}
|
||||
if ($type === null) {
|
||||
throw new \Exception("Couldn't detect a transformer model from your doc block. Did you remember to specify a model using @transformerModel?");
|
||||
}
|
||||
|
||||
$configuredStrategies = $this->config->get('examples.models_source', ['factoryCreate', 'factoryMake', 'databaseFirst']);
|
||||
|
||||
$strategies = [
|
||||
'factoryCreate' => fn () => $this->getExampleModelFromFactoryCreate($type, $factoryStates, $relations, $withCount),
|
||||
'factoryCreateQuietly' => fn () => $this->getExampleModelFromFactoryCreate($type, $factoryStates, $relations, $withCount, true),
|
||||
'factoryMake' => fn () => $this->getExampleModelFromFactoryMake($type, $factoryStates, $relations),
|
||||
'databaseFirst' => fn () => $this->getExampleModelFromDatabaseFirst($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);
|
||||
}
|
||||
}
|
||||
|
||||
return new $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $type
|
||||
* @param string[] $factoryStates
|
||||
* @param string[] $relations
|
||||
* @param string[] $withCount
|
||||
* @return null|Model
|
||||
*/
|
||||
protected function getExampleModelFromFactoryCreate(string $type, array $factoryStates = [], array $relations = [], array $withCount = [], bool $quietly = false)
|
||||
{
|
||||
// Since $relations and $withCount refer to the same underlying relationships in the model,
|
||||
// combining them ensures that all required relationships are initialized when passed to the factory.
|
||||
$allRelations = array_unique(array_merge($relations, $withCount));
|
||||
|
||||
$factory = Utils::getModelFactory($type, $factoryStates, $allRelations);
|
||||
|
||||
$factory = $quietly ? Model::withoutEvents(fn () => $factory->create()) : $factory->create();
|
||||
|
||||
return $factory->refresh()->load($relations)->loadCount($withCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $type
|
||||
* @param string[] $factoryStates
|
||||
* @return null|Model
|
||||
*/
|
||||
protected function getExampleModelFromFactoryMake(string $type, array $factoryStates = [], array $relations = [])
|
||||
{
|
||||
$factory = Utils::getModelFactory($type, $factoryStates, $relations);
|
||||
|
||||
return $factory->make();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $type
|
||||
* @param string[] $relations
|
||||
* @return null|Model
|
||||
*/
|
||||
protected function getExampleModelFromDatabaseFirst(string $type, array $relations = [])
|
||||
{
|
||||
return $type::with($relations)->first();
|
||||
}
|
||||
}
|
||||
104
vendor/knuckleswtf/scribe/src/Extracting/MethodAstParser.php
vendored
Normal file
104
vendor/knuckleswtf/scribe/src/Extracting/MethodAstParser.php
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt;
|
||||
use PhpParser\NodeFinder;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\NodeVisitor\NameResolver;
|
||||
use PhpParser\ParserFactory;
|
||||
|
||||
/**
|
||||
* MethodAstParser
|
||||
* Utility class to help with retrieving (and caching) ASTs of route methods.
|
||||
*/
|
||||
class MethodAstParser
|
||||
{
|
||||
protected static array $methodAsts = [];
|
||||
|
||||
protected static array $classAsts = [];
|
||||
|
||||
public static function getMethodAst(\ReflectionFunctionAbstract $method)
|
||||
{
|
||||
$methodName = $method->name;
|
||||
$fileName = $method->getFileName();
|
||||
|
||||
$methodAst = self::getCachedMethodAst($fileName, $methodName);
|
||||
|
||||
if ($methodAst) {
|
||||
return $methodAst;
|
||||
}
|
||||
|
||||
$classAst = self::getClassAst($fileName);
|
||||
$methodAst = self::findMethodInClassAst($classAst, $methodName);
|
||||
self::cacheMethodAst($fileName, $methodName, $methodAst);
|
||||
|
||||
return $methodAst;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|Stmt[]
|
||||
*/
|
||||
protected static function parseClassSourceCode(string $sourceCode): ?array
|
||||
{
|
||||
$parser = (new ParserFactory)->createForHostVersion();
|
||||
|
||||
try {
|
||||
$ast = $parser->parse($sourceCode);
|
||||
} catch (\Throwable $error) {
|
||||
throw new \Exception("Parse error: {$error->getMessage()}");
|
||||
}
|
||||
|
||||
$traverser = new NodeTraverser(new NameResolver(options: ['replaceNodes' => false]));
|
||||
|
||||
try {
|
||||
$traverser->traverse($ast);
|
||||
} catch (\Throwable $error) {
|
||||
throw new \Exception("Traverse error: {$error->getMessage()}");
|
||||
}
|
||||
|
||||
return $ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Stmt[] $ast
|
||||
* @return null|Node
|
||||
*/
|
||||
protected static function findMethodInClassAst(array $ast, string $methodName)
|
||||
{
|
||||
$nodeFinder = new NodeFinder;
|
||||
|
||||
return $nodeFinder->findFirst($ast, function (Node $node) use ($methodName) {
|
||||
// Todo handle closures
|
||||
return $node instanceof Stmt\ClassMethod
|
||||
&& $node->name->toString() === $methodName;
|
||||
});
|
||||
}
|
||||
|
||||
protected static function getCachedMethodAst(string $fileName, string $methodName)
|
||||
{
|
||||
$key = self::getAstCacheId($fileName, $methodName);
|
||||
|
||||
return self::$methodAsts[$key] ?? null;
|
||||
}
|
||||
|
||||
protected static function cacheMethodAst(string $fileName, string $methodName, Node $methodAst)
|
||||
{
|
||||
$key = self::getAstCacheId($fileName, $methodName);
|
||||
self::$methodAsts[$key] = $methodAst;
|
||||
}
|
||||
|
||||
private static function getAstCacheId(string $fileName, string $methodName): string
|
||||
{
|
||||
return $fileName.'///'.$methodName;
|
||||
}
|
||||
|
||||
private static function getClassAst(string $fileName)
|
||||
{
|
||||
$classAst = self::$classAsts[$fileName]
|
||||
?? self::parseClassSourceCode(file_get_contents($fileName));
|
||||
|
||||
return self::$classAsts[$fileName] = $classAst;
|
||||
}
|
||||
}
|
||||
270
vendor/knuckleswtf/scribe/src/Extracting/ParamHelpers.php
vendored
Normal file
270
vendor/knuckleswtf/scribe/src/Extracting/ParamHelpers.php
vendored
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use Faker\Factory;
|
||||
use Faker\Generator;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait ParamHelpers
|
||||
{
|
||||
/**
|
||||
* Normalizes the stated "type" of a parameter (eg "int", "integer", "double", "array"...)
|
||||
* to a number of standard JSON types (integer, boolean, number, object...).
|
||||
* Will return the input if no match.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function normalizeTypeName(?string $typeName, $value = null): string
|
||||
{
|
||||
if (! $typeName) {
|
||||
return 'string';
|
||||
}
|
||||
|
||||
$base = str_replace('[]', '', mb_strtolower($typeName));
|
||||
|
||||
return match ($base) {
|
||||
'bool' => str_replace($base, 'boolean', $typeName),
|
||||
'int' => str_replace($base, 'integer', $typeName),
|
||||
'float', 'double' => str_replace($base, 'number', $typeName),
|
||||
'array' => (empty($value) || array_keys($value)[0] === 0)
|
||||
? static::normalizeTypeName(gettype($value[0] ?? '')).'[]'
|
||||
: 'object',
|
||||
default => $typeName
|
||||
};
|
||||
}
|
||||
|
||||
protected function getFakeFactoryByName(string $name): ?\Closure
|
||||
{
|
||||
$faker = $this->getFaker();
|
||||
|
||||
$name = mb_strtolower(array_reverse(explode('.', $name))[0]);
|
||||
$normalizedName = match (true) {
|
||||
Str::endsWith($name, ['email', 'email_address']) => 'email',
|
||||
Str::endsWith($name, ['uuid']) => 'uuid',
|
||||
Str::endsWith($name, ['url']) => 'url',
|
||||
Str::endsWith($name, ['locale']) => 'locale',
|
||||
Str::endsWith($name, ['timezone']) => 'timezone',
|
||||
default => $name,
|
||||
};
|
||||
|
||||
return match ($normalizedName) {
|
||||
'email' => fn () => $faker->safeEmail(),
|
||||
'password', 'pwd' => fn () => $faker->password(),
|
||||
'url' => fn () => $faker->url(),
|
||||
'description' => fn () => $faker->sentence(),
|
||||
'uuid' => fn () => $faker->uuid(),
|
||||
'locale' => fn () => $faker->locale(),
|
||||
'timezone' => fn () => $faker->timezone(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
protected function getFaker(): Generator
|
||||
{
|
||||
$faker = Factory::create();
|
||||
if ($seed = $this->config->get('examples.faker_seed')) {
|
||||
$faker->seed($seed);
|
||||
}
|
||||
|
||||
return $faker;
|
||||
}
|
||||
|
||||
protected function generateDummyValue(string $type, array $hints = [])
|
||||
{
|
||||
if (! empty($hints['enumValues'])) {
|
||||
return Arr::random($hints['enumValues']);
|
||||
}
|
||||
|
||||
$fakeFactory = $this->getDummyValueGenerator($type, $hints);
|
||||
|
||||
return $fakeFactory();
|
||||
}
|
||||
|
||||
protected function getDummyValueGenerator(string $type, array $hints = []): \Closure
|
||||
{
|
||||
$baseType = $type;
|
||||
$isListType = false;
|
||||
|
||||
if (Str::endsWith($type, '[]')) {
|
||||
$baseType = mb_strtolower(mb_substr($type, 0, mb_strlen($type) - 2));
|
||||
$isListType = true;
|
||||
}
|
||||
|
||||
$size = $hints['size'] ?? null;
|
||||
if ($isListType) {
|
||||
// Return a one-array item for a list by default.
|
||||
return $size
|
||||
? fn () => [$this->generateDummyValue($baseType, range(0, min($size - 1, 5)))]
|
||||
: fn () => [$this->generateDummyValue($baseType, $hints)];
|
||||
}
|
||||
|
||||
if (($hints['name'] ?? false) && $baseType !== 'file') {
|
||||
$fakeFactoryByName = $this->getFakeFactoryByName($hints['name']);
|
||||
if ($fakeFactoryByName) {
|
||||
return $fakeFactoryByName;
|
||||
}
|
||||
}
|
||||
|
||||
$faker = $this->getFaker();
|
||||
$min = $hints['min'] ?? null;
|
||||
$max = $hints['max'] ?? null;
|
||||
// If max and min were provided, the override size.
|
||||
$isExactSize = is_null($min) && is_null($max) && ! is_null($size);
|
||||
|
||||
$fakeFactoriesByType = [
|
||||
'integer' => function () use ($size, $isExactSize, $max, $faker, $min) {
|
||||
if ($isExactSize) {
|
||||
return $size;
|
||||
}
|
||||
|
||||
return $max ? $faker->numberBetween((int) $min, (int) $max) : $faker->numberBetween(1, 20);
|
||||
},
|
||||
'number' => function () use ($size, $isExactSize, $max, $faker, $min) {
|
||||
if ($isExactSize) {
|
||||
return $size;
|
||||
}
|
||||
|
||||
return $max ? $faker->numberBetween((int) $min, (int) $max) : $faker->randomFloat();
|
||||
},
|
||||
'boolean' => fn () => $faker->boolean(),
|
||||
'string' => fn () => $size ? $faker->lexify(str_repeat('?', $size)) : $faker->word(),
|
||||
'object' => fn () => [],
|
||||
'file' => fn () => UploadedFile::fake()->create('test.jpg')->size($size ?: 10),
|
||||
];
|
||||
|
||||
return $fakeFactoriesByType[$baseType] ?? $fakeFactoriesByType['string'];
|
||||
}
|
||||
|
||||
protected function isSupportedTypeInDocBlocks(string $type): bool
|
||||
{
|
||||
$types = [
|
||||
'integer',
|
||||
'int',
|
||||
'number',
|
||||
'float',
|
||||
'double',
|
||||
'boolean',
|
||||
'bool',
|
||||
'string',
|
||||
'object',
|
||||
];
|
||||
|
||||
return in_array(str_replace('[]', '', $type), $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast a value to a specified type.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
protected function castToType($value, string $type)
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type === 'array') {
|
||||
$type = 'string[]';
|
||||
}
|
||||
|
||||
if (Str::endsWith($type, '[]')) {
|
||||
$baseType = mb_strtolower(mb_substr($type, 0, mb_strlen($type) - 2));
|
||||
|
||||
return is_array($value) ? array_map(function ($v) use ($baseType) {
|
||||
return $this->castToType($v, $baseType);
|
||||
}, $value) : json_decode($value);
|
||||
}
|
||||
|
||||
if ($type === 'object') {
|
||||
return is_array($value) ? $value : json_decode($value, true);
|
||||
}
|
||||
|
||||
$casts = [
|
||||
'integer' => 'intval',
|
||||
'int' => 'intval',
|
||||
'float' => 'floatval',
|
||||
'number' => 'floatval',
|
||||
'double' => 'floatval',
|
||||
'boolean' => 'boolval',
|
||||
'bool' => 'boolval',
|
||||
];
|
||||
|
||||
// First, we handle booleans. We can't use a regular cast,
|
||||
// because PHP considers string 'false' as true.
|
||||
if ($value === 'false' && ($type === 'boolean' || $type === 'bool')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($casts[$type])) {
|
||||
return $casts[$type]($value);
|
||||
}
|
||||
|
||||
// Return the value unchanged if there's no applicable cast
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows users to specify that we shouldn't generate an example for the parameter
|
||||
* by writing 'No-example'.
|
||||
*
|
||||
* @return bool if true, don't generate an example for this
|
||||
*/
|
||||
protected function shouldExcludeExample(string $description): bool
|
||||
{
|
||||
return mb_strpos($description, ' No-example') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows users to specify an example for the parameter by writing 'Example: the-example',
|
||||
* to be used in example requests and response calls.
|
||||
*
|
||||
* @param string $type The type of the parameter. Used to cast the example provided, if any.
|
||||
* @return array the description and included example
|
||||
*/
|
||||
protected function parseExampleFromParamDescription(string $description, string $type): array
|
||||
{
|
||||
$exampleWasSpecified = false;
|
||||
$example = null;
|
||||
$enumValues = [];
|
||||
|
||||
if (preg_match('/(.*)\bExample:\s*([\s\S]+)\s*/s', $description, $content)) {
|
||||
$exampleWasSpecified = true;
|
||||
$description = mb_trim($content[1]);
|
||||
|
||||
if ($content[2] === 'null') {
|
||||
// If we intentionally put null as example we return null as example
|
||||
$example = null;
|
||||
} else {
|
||||
// Examples are parsed as strings by default, we need to cast them properly
|
||||
$example = $this->castToType($content[2], $type);
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/(.*)\bEnum:\s*([\s\S]+)\s*/s', $description, $content)) {
|
||||
$description = mb_trim($content[1]);
|
||||
|
||||
$enumValues = array_map(
|
||||
fn ($value) => $this->castToType(mb_trim($value), $type),
|
||||
explode(',', mb_rtrim(mb_trim($content[2]), '.'))
|
||||
);
|
||||
}
|
||||
|
||||
return [$description, $example, $enumValues, $exampleWasSpecified];
|
||||
}
|
||||
|
||||
private function getDummyDataGeneratorBetween(string $type, $min, $max = 90, ?string $fieldName = null): \Closure
|
||||
{
|
||||
$hints = [
|
||||
'name' => $fieldName,
|
||||
'size' => $this->getFaker()->numberBetween($min, $max),
|
||||
'min' => $min,
|
||||
'max' => $max,
|
||||
];
|
||||
|
||||
return $this->getDummyValueGenerator($type, $hints);
|
||||
}
|
||||
}
|
||||
992
vendor/knuckleswtf/scribe/src/Extracting/ParsesValidationRules.php
vendored
Normal file
992
vendor/knuckleswtf/scribe/src/Extracting/ParsesValidationRules.php
vendored
Normal file
@@ -0,0 +1,992 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ClosureValidationRule;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
use Knuckles\Scribe\Exceptions\CouldntProcessValidationRule;
|
||||
use Knuckles\Scribe\Exceptions\ProblemParsingValidationRules;
|
||||
use Knuckles\Scribe\Exceptions\ScribeException;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\WritingUtils as w;
|
||||
|
||||
trait ParsesValidationRules
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
public static \stdClass $MISSING_VALUE;
|
||||
|
||||
public function getParametersFromValidationRules(array $validationRulesByParameters, array $customParameterData = []): array
|
||||
{
|
||||
self::$MISSING_VALUE = new \stdClass;
|
||||
$validationRulesByParameters = $this->normaliseRules($validationRulesByParameters);
|
||||
|
||||
$parameters = [];
|
||||
$rulesWhichDependOnType = ['between', 'max', 'min', 'size', 'gt', 'gte', 'lt', 'lte', 'before', 'after', 'before_or_equal', 'after_or_equal'];
|
||||
foreach ($validationRulesByParameters as $parameter => $ruleset) {
|
||||
$userSpecifiedParameterInfo = $customParameterData[$parameter] ?? [];
|
||||
$stringRules = array_filter($ruleset, fn ($rule) => is_string($rule));
|
||||
$rulesAndArguments = array_map(fn ($rule) => $this->parseStringRuleIntoRuleAndArguments($rule), $stringRules);
|
||||
|
||||
try {
|
||||
$this->warnAboutMissingCustomParameterData($parameter, $customParameterData);
|
||||
|
||||
// Make sure the user-specified description comes first (and add full stops where needed).
|
||||
$description = $userSpecifiedParameterInfo['description'] ?? '';
|
||||
if (! empty($description) && ! Str::endsWith($description, '.')) {
|
||||
$description .= '.';
|
||||
}
|
||||
$parameterData = [
|
||||
'name' => $parameter,
|
||||
'required' => false,
|
||||
'sometimes' => false,
|
||||
'type' => null,
|
||||
'example' => self::$MISSING_VALUE,
|
||||
'description' => $description,
|
||||
'nullable' => false,
|
||||
];
|
||||
|
||||
$closureRules = array_filter($ruleset, fn ($rule) => ($rule instanceof ClosureValidationRule || $rule instanceof Closure));
|
||||
foreach ($closureRules as $rule) {
|
||||
$this->processClosureRule($rule, $parameterData);
|
||||
}
|
||||
|
||||
$enumValidationRules = array_filter($ruleset, fn ($rule) => $rule instanceof Enum);
|
||||
foreach ($enumValidationRules as $rule) {
|
||||
$this->processEnumValidationRule($rule, $parameterData);
|
||||
}
|
||||
|
||||
$ruleObjects = array_filter($ruleset, fn ($rule) => ($rule instanceof Rule || $rule instanceof ValidationRule));
|
||||
foreach ($ruleObjects as $rule) {
|
||||
$this->processRuleObject($rule, $parameterData);
|
||||
}
|
||||
|
||||
// TODO support more rules
|
||||
// Process in 3 passes
|
||||
// 1. Rules which provide no info about type or example
|
||||
// (required, required_*, same, different, nullable, exists, and others in the "utilities" group)
|
||||
// 2. Rules which set a type.
|
||||
// 3. Rules whose processing depends on the type. ('between', 'max', 'min', 'size', 'gt', 'gte', 'lt', 'lte')
|
||||
// - Note: 'in' does not provide type info (does it?), but is enough to generate an example
|
||||
|
||||
// First pass: process rules which provide no type or example info
|
||||
$firstPassRuleNames = [
|
||||
'sometimes',
|
||||
'required',
|
||||
'required_*',
|
||||
'accepted',
|
||||
'same',
|
||||
'different',
|
||||
'nullable',
|
||||
];
|
||||
|
||||
$firstPassRules = array_filter($rulesAndArguments, fn ($ruleAndArgs) => Str::is($firstPassRuleNames, $ruleAndArgs[0]));
|
||||
foreach ($firstPassRules as $ruleAndArgs) {
|
||||
$this->processRule($ruleAndArgs[0], $ruleAndArgs[1], $parameterData, $validationRulesByParameters);
|
||||
}
|
||||
|
||||
$secondPassRules = array_filter($rulesAndArguments, fn ($ruleAndArgs) => ! Str::is($firstPassRuleNames, $ruleAndArgs[0]) && ! in_array($ruleAndArgs[0], $rulesWhichDependOnType));
|
||||
foreach ($secondPassRules as $ruleAndArgs) {
|
||||
$this->processRule($ruleAndArgs[0], $ruleAndArgs[1], $parameterData, $validationRulesByParameters);
|
||||
}
|
||||
|
||||
// The second pass should have set a type. If not, set a default type
|
||||
if (is_null($parameterData['type'])) {
|
||||
$parameterData['type'] = 'string';
|
||||
}
|
||||
|
||||
if ($parameterData['required'] === true) {
|
||||
$parameterData['nullable'] = false;
|
||||
}
|
||||
|
||||
// Now parse any "dependent" rules and set examples. At this point, we should know all field's types.
|
||||
$thirdPassRules = array_filter($rulesAndArguments, fn ($ruleAndArgs) => in_array($ruleAndArgs[0], $rulesWhichDependOnType));
|
||||
foreach ($thirdPassRules as $ruleAndArgs) {
|
||||
$this->processRule($ruleAndArgs[0], $ruleAndArgs[1], $parameterData, $validationRulesByParameters);
|
||||
}
|
||||
|
||||
// Make sure the user-specified example overwrites ours.
|
||||
if (array_key_exists('example', $userSpecifiedParameterInfo)) {
|
||||
if ($userSpecifiedParameterInfo['example'] !== null && $this->shouldCastUserExample()) {
|
||||
// Examples in comments are strings, we need to cast them properly
|
||||
$parameterData['example'] = $this->castToType($userSpecifiedParameterInfo['example'], $parameterData['type'] ?? 'string');
|
||||
} else {
|
||||
$parameterData['example'] = $userSpecifiedParameterInfo['example'];
|
||||
}
|
||||
}
|
||||
|
||||
// End descriptions with a full stop
|
||||
if (! empty($parameterData['description']) && ! Str::endsWith($parameterData['description'], '.')) {
|
||||
$parameterData['description'] .= '.';
|
||||
}
|
||||
|
||||
$parameterData['description'] = mb_trim($parameterData['description']);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel uses .* notation for arrays. This PR aims to normalise that into our "new syntax".
|
||||
*
|
||||
* 'years.*' with type 'integer' becomes 'years' with type 'integer[]'
|
||||
* 'cars.*.age' with type 'string' becomes 'cars[].age' with type 'string' and 'cars' with type 'object[]'
|
||||
* 'cars.*.things.*.*' with type 'string' becomes 'cars[].things' with type 'string[][]' and 'cars' with type
|
||||
* 'object[]'
|
||||
*
|
||||
* Additionally, if the user declared a subfield but not the parent, we create a parameter for the parent.
|
||||
*
|
||||
* @param array[] $parametersFromValidationRules
|
||||
*/
|
||||
public function normaliseArrayAndObjectParameters(array $parametersFromValidationRules): array
|
||||
{
|
||||
// Convert any `array` types into concrete types like `object[]`, object, or `string[]`
|
||||
$parameters = $this->convertGenericArrayType($parametersFromValidationRules);
|
||||
|
||||
// Change cars.*.dogs.things.*.* with type X to cars.*.dogs.things with type X[][]
|
||||
$parameters = $this->convertArraySubfields($parameters);
|
||||
|
||||
// Add the fields `cars.*.dogs` and `cars` if they don't exist
|
||||
$parameters = $this->addMissingParentFields($parameters);
|
||||
|
||||
return $this->setExamples($parameters);
|
||||
}
|
||||
|
||||
public function convertGenericArrayType(array $parameters): array
|
||||
{
|
||||
$converted = [];
|
||||
$allKeys = array_keys($parameters);
|
||||
foreach (array_reverse($parameters) as $name => $details) {
|
||||
if ($details['type'] === 'array') {
|
||||
// This is a parent field, a generic array type. Scribe only supports concrete array types (T[]),
|
||||
// so we convert this to the correct type (such as object or object[])
|
||||
|
||||
// Supposing current key is "users", with type "array". To fix this:
|
||||
// 1. If `users.*` or `users.*.thing` exists, `users` is an `X[]` (where X is the type of `users.*`
|
||||
// 2. If `users.<name>` exists, `users` is an `object`
|
||||
// 3. Otherwise, default to `object`
|
||||
// Important: We're iterating in reverse, to ensure we set child items before parent items
|
||||
// (assuming the user specified parents first, which is the more common thing)y
|
||||
if (Arr::first($allKeys, fn ($key) => Str::startsWith($key, "{$name}.*."))) {
|
||||
$details['type'] = 'object[]';
|
||||
unset($details['setter']);
|
||||
} elseif ($childKey = Arr::first($allKeys, fn ($key) => Str::startsWith($key, "{$name}.*"))) {
|
||||
$childType = ($converted[$childKey] ?? $parameters[$childKey])['type'];
|
||||
$details['type'] = "{$childType}[]";
|
||||
} else { // `array` types default to `object` if no subtype is specified
|
||||
$details['type'] = 'object';
|
||||
unset($details['setter']);
|
||||
}
|
||||
}
|
||||
|
||||
$converted[$name] = $details;
|
||||
}
|
||||
|
||||
// Re-add items in the original order, so as to not cause side effects
|
||||
foreach ($allKeys as $key) {
|
||||
$parameters[$key] = $converted[$key] ?? $parameters[$key];
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
public function convertArraySubfields(array $parameters): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($parameters as $name => $details) {
|
||||
if (Str::endsWith($name, '.*')) {
|
||||
// The user might have set the example via bodyParameters()
|
||||
$exampleWasSpecified = $this->examplePresent($details);
|
||||
|
||||
// Change cars.*.dogs.things.*.* with type X to cars.*.dogs.things with type X[][]
|
||||
while (Str::endsWith($name, '.*')) {
|
||||
$details['type'] .= '[]';
|
||||
$name = mb_substr($name, 0, -2);
|
||||
|
||||
if ($exampleWasSpecified) {
|
||||
$details['example'] = [$details['example']];
|
||||
} elseif (isset($details['setter'])) {
|
||||
$previousSetter = $details['setter'];
|
||||
$details['setter'] = fn () => [$previousSetter()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$results[$name] = $details;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function setExamples(array $parameters): array
|
||||
{
|
||||
$examples = [];
|
||||
|
||||
foreach ($parameters as $name => $details) {
|
||||
if ($this->examplePresent($details)) {
|
||||
// Record already-present examples (eg from bodyParameters()).
|
||||
// This allows a user to set 'data' => ['example' => ['title' => 'A title'],
|
||||
// and we automatically set this as the example for `data.title`
|
||||
// Note that this approach assumes parent fields are listed before the children; meh.
|
||||
$examples[$details['name']] = $details['example'];
|
||||
} elseif (preg_match('/.+\.[^*]+$/', $details['name'])) {
|
||||
// For object fields (eg 'data.details.title'), set examples from their parents if present as described above.
|
||||
[$parentName, $fieldName] = preg_split('/\.(?=[\w-]+$)/', $details['name']);
|
||||
if (array_key_exists($parentName, $examples) && is_array($examples[$parentName])
|
||||
&& array_key_exists($fieldName, $examples[$parentName])) {
|
||||
$examples[$details['name']] = $details['example'] = $examples[$parentName][$fieldName];
|
||||
}
|
||||
}
|
||||
|
||||
$details['example'] = $this->getParameterExample($details);
|
||||
unset($details['setter']);
|
||||
|
||||
$parameters[$name] = $details;
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform validation rules:
|
||||
* 1. from strings to arrays:
|
||||
* 'param1' => 'int|required' TO 'param1' => ['int', 'required']
|
||||
* 2. from '*.foo' to '
|
||||
*
|
||||
* @param array<string,string|string[]> $rules
|
||||
*/
|
||||
protected function normaliseRules(array $rules): array
|
||||
{
|
||||
// We can simply call Validator::make($data, $rules)->getRules() to get the normalised rules,
|
||||
// but Laravel will ignore any nested array rules (`ids.*')
|
||||
// unless the key referenced (`ids`) exists in the dataset and is a non-empty array
|
||||
// So we'll create a single-item array for each array parameter
|
||||
$testData = [];
|
||||
foreach ($rules as $key => $ruleset) {
|
||||
if (! Str::contains($key, '.*')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// All we need is for Laravel to see this key exists
|
||||
Arr::set($testData, str_replace('.*', '.0', $key), Str::random());
|
||||
}
|
||||
|
||||
// Now this will return the complete ruleset.
|
||||
// Nested array parameters will be present, with '*' replaced by '0'
|
||||
$newRules = Validator::make($testData, $rules)->getRules();
|
||||
|
||||
return collect($newRules)->mapWithKeys(function ($val, $paramName) use ($rules) {
|
||||
// Transform the key names back from '__asterisk__' to '*'
|
||||
if (Str::contains($paramName, '__asterisk__')) {
|
||||
// In Laravel < v11.44, * keys were replaced with only "__asterisk__"
|
||||
// After that, * keys were replaced with "__asterisk__<random placeholder>", eg "__asterisk__dkjiu78gujjhb
|
||||
// See https://github.com/laravel/framework/pull/54845/
|
||||
$paramName = preg_replace('/__asterisk__[^.]*\b/', '*', $paramName);
|
||||
}
|
||||
|
||||
// Transform the key names back from 'ids.0' to 'ids.*'
|
||||
if (Str::contains($paramName, '.0')) {
|
||||
$genericArrayKeyName = str_replace('.0', '.*', $paramName);
|
||||
|
||||
// But only if that was the original value
|
||||
if (isset($rules[$genericArrayKeyName])) {
|
||||
$paramName = $genericArrayKeyName;
|
||||
}
|
||||
}
|
||||
|
||||
return [$paramName => $val];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
// For inline Closure rules, turn any comment above it into a description
|
||||
//
|
||||
// $request->validate([
|
||||
// 'my_param' => [
|
||||
// 'required',
|
||||
// /** Must be a hexadecimal number. */
|
||||
// function ($attribute, $value, $fail) {
|
||||
// if (!preg_match('/^[0-9a-f]+$/', $value)) {
|
||||
// $fail('Must be in hex format');
|
||||
// }
|
||||
// },
|
||||
// ],
|
||||
// ]);
|
||||
protected function processClosureRule($rule, array &$parameterData): void
|
||||
{
|
||||
$docComment = (new \ReflectionFunction($rule instanceof ClosureValidationRule ? $rule->callback : $rule))
|
||||
->getDocComment();
|
||||
|
||||
if (is_string($docComment)) {
|
||||
$description = '';
|
||||
foreach (explode("\n", $docComment) as $line) {
|
||||
$cleaned = preg_replace(['/\*+\/$/', '/^\/\*+\s*/', '/^\*+\s*/'], '', mb_trim($line));
|
||||
if ($cleaned !== '') {
|
||||
$description .= ' '.$cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
$parameterData['description'] .= $description;
|
||||
}
|
||||
}
|
||||
|
||||
protected function processEnumValidationRule($rule, array &$parameterData, array $allParameters = []): void
|
||||
{
|
||||
$property = (new \ReflectionClass($rule))->getProperty('type');
|
||||
$property->setAccessible(true);
|
||||
$enumClass = $property->getValue($rule);
|
||||
|
||||
if (enum_exists($enumClass) && method_exists($enumClass, 'tryFrom')) {
|
||||
// $case->value only exists on BackedEnums, not UnitEnums
|
||||
// method_exists($enum, 'tryFrom') implies the enum is a BackedEnum
|
||||
// @phpstan-ignore-next-line
|
||||
$cases = array_map(fn ($case) => $case->value, $enumClass::cases());
|
||||
$parameterData['type'] = gettype($cases[0]);
|
||||
$parameterData['enumValues'] = $cases;
|
||||
$parameterData['setter'] = fn () => Arr::random($cases);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processRuleObject($rule, array &$parameterData): void
|
||||
{
|
||||
if (method_exists($rule, 'invokable')) {
|
||||
// Laravel wraps InvokableRule instances in an InvokableValidationRule class,
|
||||
// so we must retrieve the original rule
|
||||
$rule = $rule->invokable();
|
||||
}
|
||||
|
||||
// Users can define a custom "docs" method on a rule to give Scribe more info.
|
||||
if (method_exists($rule, 'docs')) {
|
||||
$customData = call_user_func_array([$rule, 'docs'], []) ?: [];
|
||||
|
||||
if (isset($customData['description'])) {
|
||||
$parameterData['description'] .= ' '.$customData['description'];
|
||||
}
|
||||
if (isset($customData['example'])) {
|
||||
$parameterData['setter'] = fn () => $customData['example'];
|
||||
} elseif (isset($customData['setter'])) {
|
||||
$parameterData['setter'] = $customData['setter'];
|
||||
}
|
||||
|
||||
$parameterData = array_merge($parameterData, Arr::except($customData, [
|
||||
'description', 'example', 'setter',
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a validation rule and extract a parameter type, description and setter (used to generate an example).
|
||||
*
|
||||
* @param array $allParameters all parameters, used to check if an argument in the date rules (eg `before:some_date`)
|
||||
* is a parameter in the request body
|
||||
* @param mixed $rule
|
||||
* @param mixed $ruleArguments
|
||||
*/
|
||||
protected function processRule($rule, $ruleArguments, array &$parameterData, array $allParameters = []): bool
|
||||
{
|
||||
// Reminder: Always append to the description (with a leading space); don't overwrite.
|
||||
try {
|
||||
switch ($rule) {
|
||||
case 'sometimes':
|
||||
$parameterData['sometimes'] = true;
|
||||
|
||||
break;
|
||||
|
||||
case 'required':
|
||||
if (! $parameterData['sometimes']) {
|
||||
$parameterData['required'] = true;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'accepted':
|
||||
if (! $parameterData['sometimes']) {
|
||||
$parameterData['required'] = true;
|
||||
}
|
||||
$parameterData['type'] = 'boolean';
|
||||
$parameterData['description'] .= ' Must be accepted.';
|
||||
$parameterData['setter'] = fn () => true;
|
||||
|
||||
break;
|
||||
|
||||
case 'accepted_if':
|
||||
$parameterData['type'] = 'boolean';
|
||||
$parameterData['description'] .= " Must be accepted when <code>{$ruleArguments[0]}</code> is ".w::getListOfValuesAsFriendlyHtmlString(array_slice($ruleArguments, 1));
|
||||
$parameterData['setter'] = fn () => true;
|
||||
|
||||
break;
|
||||
|
||||
// Primitive types. No description should be added
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
$parameterData['setter'] = function () {
|
||||
return Arr::random([true, false]);
|
||||
};
|
||||
$parameterData['type'] = 'boolean';
|
||||
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
$parameterData['setter'] = function () use ($parameterData) {
|
||||
return $this->generateDummyValue('string', ['name' => $parameterData['name']]);
|
||||
};
|
||||
$parameterData['type'] = 'string';
|
||||
|
||||
break;
|
||||
|
||||
case 'int':
|
||||
case 'integer':
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('integer');
|
||||
};
|
||||
$parameterData['type'] = 'integer';
|
||||
|
||||
break;
|
||||
|
||||
case 'numeric':
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('number');
|
||||
};
|
||||
$parameterData['type'] = 'number';
|
||||
|
||||
break;
|
||||
|
||||
case 'array':
|
||||
$parameterData['setter'] = function () {
|
||||
return [$this->generateDummyValue('string')];
|
||||
};
|
||||
$parameterData['type'] = 'array'; // The cleanup code in normaliseArrayAndObjectParameters() will set this to a valid type (x[] or object)
|
||||
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
$parameterData['type'] = 'file';
|
||||
$parameterData['description'] .= ' Must be a file.';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->generateDummyValue('file');
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
// Special string types
|
||||
case 'alpha':
|
||||
$parameterData['description'] .= ' Must contain only letters.';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->lexify('??????');
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case 'alpha_dash':
|
||||
$parameterData['description'] .= ' Must contain only letters, numbers, dashes and underscores.';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->lexify('???-???_?');
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case 'alpha_num':
|
||||
$parameterData['description'] .= ' Must contain only letters and numbers.';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->bothify('#?#???#');
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case 'timezone':
|
||||
// Laravel's message merely says "The value must be a valid zone"
|
||||
$parameterData['description'] .= ' Must be a valid time zone, such as <code>Africa/Accra</code>.';
|
||||
$parameterData['setter'] = $this->getFakeFactoryByName('timezone');
|
||||
|
||||
break;
|
||||
|
||||
case 'email':
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule);
|
||||
$parameterData['setter'] = $this->getFakeFactoryByName('email');
|
||||
$parameterData['type'] = 'string';
|
||||
|
||||
break;
|
||||
|
||||
case 'url':
|
||||
$parameterData['setter'] = $this->getFakeFactoryByName('url');
|
||||
$parameterData['type'] = 'string';
|
||||
// Laravel's message is "The value format is invalid". Ugh.🤮
|
||||
$parameterData['description'] .= ' Must be a valid URL.';
|
||||
|
||||
break;
|
||||
|
||||
case 'ip':
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule);
|
||||
$parameterData['type'] = 'string';
|
||||
$parameterData['setter'] = function () {
|
||||
return $this->getFaker()->ipv4();
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case 'json':
|
||||
$parameterData['type'] = 'string';
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule);
|
||||
$parameterData['setter'] = function () {
|
||||
return json_encode([$this->getFaker()->word(), $this->getFaker()->word()]);
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
$parameterData['type'] = 'string';
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule);
|
||||
$parameterData['setter'] = fn () => date('Y-m-d\TH:i:s', time());
|
||||
|
||||
break;
|
||||
|
||||
case 'date_format':
|
||||
$parameterData['type'] = 'string';
|
||||
// Laravel description here is "The value must match the format Y-m-d". Not descriptive enough.
|
||||
$parameterData['description'] .= " Must be a valid date in the format <code>{$ruleArguments[0]}</code>.";
|
||||
$parameterData['setter'] = function () use ($ruleArguments) {
|
||||
return date($ruleArguments[0], time());
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case 'after':
|
||||
case 'after_or_equal':
|
||||
$parameterData['type'] = 'string';
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule, [':date' => "<code>{$ruleArguments[0]}</code>"]);
|
||||
// TODO introduce the concept of "modifiers", like date_format
|
||||
// The startDate may refer to another field, in which case, we just ignore it for now.
|
||||
$startDate = array_key_exists($ruleArguments[0], $allParameters) ? 'today' : $ruleArguments[0];
|
||||
$parameterData['setter'] = fn () => $this->getFaker()->dateTimeBetween($startDate, '+100 years')->format('Y-m-d');
|
||||
|
||||
break;
|
||||
|
||||
case 'before':
|
||||
case 'before_or_equal':
|
||||
$parameterData['type'] = 'string';
|
||||
// The argument can be either another field or a date
|
||||
// The endDate may refer to another field, in which case, we just ignore it for now.
|
||||
$endDate = array_key_exists($ruleArguments[0], $allParameters) ? 'today' : $ruleArguments[0];
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule, [':date' => "<code>{$ruleArguments[0]}</code>"]);
|
||||
$parameterData['setter'] = fn () => $this->getFaker()->dateTimeBetween('-30 years', $endDate)->format('Y-m-d');
|
||||
|
||||
break;
|
||||
|
||||
case 'starts_with':
|
||||
$parameterData['description'] .= ' Must start with one of '.w::getListOfValuesAsFriendlyHtmlString($ruleArguments);
|
||||
$parameterData['setter'] = fn () => $this->getFaker()->lexify("{$ruleArguments[0]}????");
|
||||
|
||||
break;
|
||||
|
||||
case 'ends_with':
|
||||
$parameterData['description'] .= ' Must end with one of '.w::getListOfValuesAsFriendlyHtmlString($ruleArguments);
|
||||
$parameterData['setter'] = fn () => $this->getFaker()->lexify("????{$ruleArguments[0]}");
|
||||
|
||||
break;
|
||||
|
||||
case 'uuid':
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule).' ';
|
||||
$parameterData['setter'] = $this->getFakeFactoryByName('uuid');
|
||||
|
||||
break;
|
||||
|
||||
case 'regex':
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule, [':regex' => $ruleArguments[0]]);
|
||||
$parameterData['setter'] = fn () => $this->getFaker()->regexify($ruleArguments[0]);
|
||||
|
||||
break;
|
||||
|
||||
// Special number types.
|
||||
case 'digits':
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule, [':digits' => $ruleArguments[0]]);
|
||||
$parameterData['setter'] = fn () => $this->getFaker()->numerify(str_repeat('#', $ruleArguments[0]));
|
||||
$parameterData['type'] = 'string';
|
||||
|
||||
break;
|
||||
|
||||
case 'digits_between':
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule, [':min' => $ruleArguments[0], ':max' => $ruleArguments[1]]);
|
||||
$parameterData['setter'] = fn () => $this->getFaker()->numerify(str_repeat('#', rand($ruleArguments[0], $ruleArguments[1])));
|
||||
$parameterData['type'] = 'string';
|
||||
|
||||
break;
|
||||
|
||||
// These rules can apply to numbers, strings, arrays or files
|
||||
case 'size':
|
||||
$parameterData['description'] .= ' '.$this->getDescription(
|
||||
$rule,
|
||||
[':size' => $ruleArguments[0]],
|
||||
$this->getLaravelValidationBaseTypeMapping($parameterData['type'])
|
||||
);
|
||||
$parameterData['setter'] = $this->getDummyValueGenerator($parameterData['type'], ['size' => $ruleArguments[0]]);
|
||||
|
||||
break;
|
||||
|
||||
case 'min':
|
||||
$parameterData['description'] .= ' '.$this->getDescription(
|
||||
$rule,
|
||||
[':min' => $ruleArguments[0]],
|
||||
$this->getLaravelValidationBaseTypeMapping($parameterData['type'])
|
||||
);
|
||||
$parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], (float) ($ruleArguments[0]), fieldName: $parameterData['name']);
|
||||
|
||||
break;
|
||||
|
||||
case 'max':
|
||||
$parameterData['description'] .= ' '.$this->getDescription(
|
||||
$rule,
|
||||
[':max' => $ruleArguments[0]],
|
||||
$this->getLaravelValidationBaseTypeMapping($parameterData['type'])
|
||||
);
|
||||
$max = min($ruleArguments[0], 25);
|
||||
$parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], 1, $max, $parameterData['name']);
|
||||
|
||||
break;
|
||||
|
||||
case 'between':
|
||||
$parameterData['description'] .= ' '.$this->getDescription(
|
||||
$rule,
|
||||
[':min' => $ruleArguments[0], ':max' => $ruleArguments[1]],
|
||||
$this->getLaravelValidationBaseTypeMapping($parameterData['type'])
|
||||
);
|
||||
// Avoid exponentially complex operations by using the minimum length
|
||||
$parameterData['setter'] = $this->getDummyDataGeneratorBetween($parameterData['type'], (float) ($ruleArguments[0]), (float) ($ruleArguments[0]) + 1, $parameterData['name']);
|
||||
|
||||
break;
|
||||
|
||||
// Special file types.
|
||||
case 'image':
|
||||
$parameterData['type'] = 'file';
|
||||
$parameterData['description'] .= ' '.$this->getDescription($rule).' ';
|
||||
$parameterData['setter'] = function () {
|
||||
// This is fine because the file example generator generates an image
|
||||
return $this->generateDummyValue('file');
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
// Other rules.
|
||||
case 'in':
|
||||
// Cast numeric values in enumValues, but don't change the parameter type
|
||||
// The type should remain as set by other rules (eg 'string' rule) or default 'string'
|
||||
$castedValues = $ruleArguments;
|
||||
$allNumeric = count($ruleArguments) > 0 && array_reduce(
|
||||
$ruleArguments,
|
||||
fn ($carry, $val) => $carry && is_numeric($val),
|
||||
true
|
||||
);
|
||||
|
||||
if ($allNumeric) {
|
||||
// Check if all are integers (no decimal points)
|
||||
$allIntegers = array_reduce(
|
||||
$ruleArguments,
|
||||
fn ($carry, $val) => $carry && ! Str::contains($val, '.'),
|
||||
true
|
||||
);
|
||||
|
||||
$castedValues = $allIntegers
|
||||
? array_map(fn ($v) => (int) $v, $ruleArguments)
|
||||
: array_map(fn ($v) => (float) $v, $ruleArguments);
|
||||
}
|
||||
|
||||
$parameterData['enumValues'] = $castedValues;
|
||||
$parameterData['setter'] = function () use ($castedValues) {
|
||||
return Arr::random($castedValues);
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
// These rules only add a description. Generating valid examples is too complex.
|
||||
case 'not_in':
|
||||
$parameterData['description'] .= ' Must not be one of '.w::getListOfValuesAsFriendlyHtmlString($ruleArguments).' ';
|
||||
|
||||
break;
|
||||
|
||||
case 'required_if':
|
||||
$parameterData['description'] .= sprintf(
|
||||
" This field is required when <code>{$ruleArguments[0]}</code> is %s. ",
|
||||
w::getListOfValuesAsFriendlyHtmlString(array_slice($ruleArguments, 1))
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'required_unless':
|
||||
$parameterData['description'] .= sprintf(
|
||||
" This field is required unless <code>{$ruleArguments[0]}</code> is in %s. ",
|
||||
w::getListOfValuesAsFriendlyHtmlString(array_slice($ruleArguments, 1))
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'required_with':
|
||||
$parameterData['description'] .= sprintf(
|
||||
' This field is required when %s is present. ',
|
||||
w::getListOfValuesAsFriendlyHtmlString($ruleArguments)
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'required_without':
|
||||
$parameterData['description'] .= sprintf(
|
||||
' This field is required when %s is not present. ',
|
||||
w::getListOfValuesAsFriendlyHtmlString($ruleArguments)
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'required_with_all':
|
||||
$parameterData['description'] .= sprintf(
|
||||
' This field is required when %s are present. ',
|
||||
w::getListOfValuesAsFriendlyHtmlString($ruleArguments, 'and')
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'required_without_all':
|
||||
$parameterData['description'] .= sprintf(
|
||||
' This field is required when none of %s are present. ',
|
||||
w::getListOfValuesAsFriendlyHtmlString($ruleArguments, 'and')
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'same':
|
||||
$parameterData['description'] .= " The value and <code>{$ruleArguments[0]}</code> must match.";
|
||||
|
||||
break;
|
||||
|
||||
case 'different':
|
||||
$parameterData['description'] .= " The value and <code>{$ruleArguments[0]}</code> must be different.";
|
||||
|
||||
break;
|
||||
|
||||
case 'nullable':
|
||||
$parameterData['nullable'] = true;
|
||||
|
||||
break;
|
||||
|
||||
case 'exists':
|
||||
$parameterData['description'] .= " The <code>{$ruleArguments[1]}</code> of an existing record in the {$ruleArguments[0]} table.";
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// Other rules not supported
|
||||
break;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
throw CouldntProcessValidationRule::forParam($parameterData['name'], $rule, $e);
|
||||
}
|
||||
|
||||
$parameterData['description'] = mb_trim($parameterData['description']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string rule into the base rule and arguments.
|
||||
* eg "in:1,2" becomes ["in", ["1", "2"]]
|
||||
* Laravel validation rules are specified in the format {rule}:{arguments}
|
||||
* Arguments are separated by commas.
|
||||
* For instance the rule "max:3" states that the value may only be three letters.
|
||||
*
|
||||
* @param Rule|string $rule
|
||||
*/
|
||||
protected function parseStringRuleIntoRuleAndArguments($rule): array
|
||||
{
|
||||
$ruleArguments = [];
|
||||
|
||||
if (str_contains($rule, ':')) {
|
||||
[$rule, $argumentsString] = explode(':', $rule, 2);
|
||||
|
||||
// These rules can have commas in their arguments, so we don't split on commas
|
||||
if (in_array(mb_strtolower($rule), ['regex', 'date', 'date_format'])) {
|
||||
$ruleArguments = [$argumentsString];
|
||||
} else {
|
||||
$ruleArguments = str_getcsv($argumentsString);
|
||||
}
|
||||
}
|
||||
|
||||
return [mb_strtolower(mb_trim($rule)), $ruleArguments];
|
||||
}
|
||||
|
||||
protected function getParameterExample(array $parameterData)
|
||||
{
|
||||
// If no example was given by the user, set an autogenerated example.
|
||||
// Each parsed rule returns a 'setter' function. We'll evaluate the last one.
|
||||
if ($parameterData['example'] === self::$MISSING_VALUE) {
|
||||
if (isset($parameterData['setter'])) {
|
||||
return $parameterData['setter']();
|
||||
}
|
||||
|
||||
return $parameterData['required']
|
||||
? $this->generateDummyValue($parameterData['type'])
|
||||
: null;
|
||||
}
|
||||
if (! is_null($parameterData['example']) && $parameterData['example'] !== self::$MISSING_VALUE) {
|
||||
if ($parameterData['example'] === 'No-example' && ! $parameterData['required']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Casting again is important since values may have been cast to string in the validator
|
||||
return $this->castToType($parameterData['example'], $parameterData['type']);
|
||||
}
|
||||
|
||||
return $parameterData['example'] === self::$MISSING_VALUE ? null : $parameterData['example'];
|
||||
}
|
||||
|
||||
protected function addMissingParentFields(array $parameters): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($parameters as $name => $details) {
|
||||
if (isset($results[$name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parentPath = $name;
|
||||
while (Str::contains($parentPath, '.')) {
|
||||
$parentPath = preg_replace('/\.[^.]+$/', '', $parentPath);
|
||||
$normalisedParentPath = str_replace('.*.', '[].', $parentPath);
|
||||
|
||||
if (empty($results[$normalisedParentPath])) {
|
||||
// Parent field doesn't exist, create it.
|
||||
|
||||
if (Str::endsWith($parentPath, '.*')) {
|
||||
$parentPath = mb_substr($parentPath, 0, -2);
|
||||
$normalisedParentPath = str_replace('.*.', '[].', $parentPath);
|
||||
|
||||
if (! empty($results[$normalisedParentPath])) {
|
||||
break;
|
||||
}
|
||||
|
||||
$type = 'object[]';
|
||||
$example = [[]];
|
||||
} else {
|
||||
$type = 'object';
|
||||
$example = [];
|
||||
}
|
||||
$results[$normalisedParentPath] = [
|
||||
'name' => $normalisedParentPath,
|
||||
'type' => $type,
|
||||
'required' => false,
|
||||
'description' => '',
|
||||
'example' => $example,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$details['name'] = $name = str_replace('.*.', '[].', $name);
|
||||
|
||||
if (isset($parameters[$details['name']]) && $this->examplePresent($parameters[$details['name']])) {
|
||||
$details['example'] = $parameters[$details['name']]['example'];
|
||||
}
|
||||
|
||||
$results[$name] = $details;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
protected function getDescription(string $rule, array $arguments = [], $baseType = 'string'): string
|
||||
{
|
||||
if ($rule === 'regex') {
|
||||
return "Must match the regex {$arguments[':regex']}.";
|
||||
}
|
||||
|
||||
$translationString = "validation.{$rule}";
|
||||
$description = trans($translationString);
|
||||
|
||||
// For rules that can apply to multiple types (eg 'max' rule), There is an array of possible messages
|
||||
// 'numeric' => 'The :attribute must not be greater than :max'
|
||||
// 'file' => 'The :attribute must have a size less than :max kilobytes'
|
||||
// Depending on the translation engine, trans may return the array, or it will fail to translate the string
|
||||
// and will need to be called with the baseType appended.
|
||||
if ($description === $translationString) {
|
||||
$translationString = "{$translationString}.{$baseType}";
|
||||
$translated = trans($translationString);
|
||||
if ($translated !== $translationString) {
|
||||
$description = $translated;
|
||||
}
|
||||
} elseif (is_array($description)) {
|
||||
$description = $description[$baseType];
|
||||
}
|
||||
|
||||
// Convert messages from failure type ("The :attribute is not a valid date.") to info ("The :attribute must be a valid date.")
|
||||
$description = str_replace(['is not', 'does not'], ['must be', 'must'], $description);
|
||||
$description = str_replace('may not', 'must not', $description);
|
||||
|
||||
foreach ($arguments as $placeholder => $argument) {
|
||||
$description = str_replace($placeholder, $argument, $description);
|
||||
}
|
||||
|
||||
// Laravel 10 added `field` to its messages: https://github.com/laravel/framework/pull/45974
|
||||
$description = str_replace('The :attribute field ', 'The value ', $description);
|
||||
|
||||
$description = preg_replace('/(?!<\W):attribute\b/', 'value', $description);
|
||||
|
||||
return str_replace(
|
||||
['The value must ', ' 1 characters', ' 1 digits', ' 1 kilobytes'],
|
||||
['Must ', ' 1 character', ' 1 digit', ' 1 kilobyte'],
|
||||
$description
|
||||
);
|
||||
}
|
||||
|
||||
protected function getMissingCustomDataMessage($parameterName)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function shouldCastUserExample()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function warnAboutMissingCustomParameterData(string $parameter, array $customParameterData): array
|
||||
{
|
||||
$parameterPlusDot = $parameter.'.';
|
||||
if (count($customParameterData) && ! isset($customParameterData[$parameter])
|
||||
&& ! Arr::first(array_keys($customParameterData), fn ($key) => str_starts_with($key, $parameterPlusDot))
|
||||
) {
|
||||
c::debug($this->getMissingCustomDataMessage($parameter));
|
||||
}
|
||||
|
||||
return $customParameterData;
|
||||
}
|
||||
|
||||
private function examplePresent(array $parameterData)
|
||||
{
|
||||
return isset($parameterData['example']) && $parameterData['example'] !== self::$MISSING_VALUE;
|
||||
}
|
||||
|
||||
private function getLaravelValidationBaseTypeMapping(string $parameterType): string
|
||||
{
|
||||
$mapping = [
|
||||
'number' => 'numeric',
|
||||
'integer' => 'numeric',
|
||||
'file' => 'file',
|
||||
'string' => 'string',
|
||||
'array' => 'array',
|
||||
];
|
||||
|
||||
if (Str::endsWith($parameterType, '[]')) {
|
||||
return 'array';
|
||||
}
|
||||
|
||||
return $mapping[$parameterType] ?? 'string';
|
||||
}
|
||||
}
|
||||
98
vendor/knuckleswtf/scribe/src/Extracting/RouteDocBlocker.php
vendored
Normal file
98
vendor/knuckleswtf/scribe/src/Extracting/RouteDocBlocker.php
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting;
|
||||
|
||||
use Illuminate\Routing\Route;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\Utils as u;
|
||||
use Mpociot\Reflection\DocBlock;
|
||||
|
||||
/**
|
||||
* Class RouteDocBlocker
|
||||
* Utility class to help with retrieving doc blocks from route classes and methods.
|
||||
* Also caches them so repeated access is faster.
|
||||
*/
|
||||
class RouteDocBlocker
|
||||
{
|
||||
protected static array $docBlocks = [];
|
||||
|
||||
/**
|
||||
* @return array{method: DocBlock, class: ?DocBlock} Method and class docblocks
|
||||
*/
|
||||
public static function getDocBlocksFromRoute(Route $route): array
|
||||
{
|
||||
[$className, $methodName] = u::getRouteClassAndMethodNames($route);
|
||||
|
||||
return static::getDocBlocks($route, $className, $methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $className
|
||||
* @param null|mixed $methodName
|
||||
* @return array{method: DocBlock, class: DocBlock} Method and class docblocks
|
||||
*/
|
||||
public static function getDocBlocks(Route $route, $className, $methodName = null): array
|
||||
{
|
||||
if (is_array($className)) {
|
||||
[$className, $methodName] = $className;
|
||||
}
|
||||
|
||||
$normalizedClassName = static::normalizeClassName($className);
|
||||
$docBlocks = self::getCachedDocBlock($route, $normalizedClassName, $methodName);
|
||||
|
||||
if ($docBlocks) {
|
||||
return $docBlocks;
|
||||
}
|
||||
|
||||
$class = new \ReflectionClass($className);
|
||||
|
||||
if (! $class->hasMethod($methodName)) {
|
||||
throw new \Exception('Error while fetching docblock for route '.c::getRouteRepresentation($route).": Class {$className} does not contain method {$methodName}");
|
||||
}
|
||||
|
||||
$method = u::getReflectedRouteMethod([$className, $methodName]);
|
||||
|
||||
$docBlocks = [
|
||||
'method' => new DocBlock($method->getDocComment() ?: ''),
|
||||
'class' => new DocBlock($class->getDocComment() ?: ''),
|
||||
];
|
||||
self::cacheDocBlocks($route, $normalizedClassName, $methodName, $docBlocks);
|
||||
|
||||
return $docBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object|string $classNameOrInstance
|
||||
*/
|
||||
protected static function normalizeClassName($classNameOrInstance): string
|
||||
{
|
||||
if (is_object($classNameOrInstance)) {
|
||||
// Route handlers are not destroyed until the script ends so this should be perfectly safe.
|
||||
$classNameOrInstance = get_class($classNameOrInstance).'::'.spl_object_id($classNameOrInstance);
|
||||
}
|
||||
|
||||
return $classNameOrInstance;
|
||||
}
|
||||
|
||||
protected static function getCachedDocBlock(Route $route, string $className, string $methodName)
|
||||
{
|
||||
$routeId = self::getRouteCacheId($route, $className, $methodName);
|
||||
|
||||
return self::$docBlocks[$routeId] ?? null;
|
||||
}
|
||||
|
||||
protected static function cacheDocBlocks(Route $route, string $className, string $methodName, array $docBlocks)
|
||||
{
|
||||
$routeId = self::getRouteCacheId($route, $className, $methodName);
|
||||
self::$docBlocks[$routeId] = $docBlocks;
|
||||
}
|
||||
|
||||
private static function getRouteCacheId(Route $route, string $className, string $methodName): string
|
||||
{
|
||||
return $route->uri()
|
||||
.':'
|
||||
.implode(array_diff($route->methods(), ['HEAD']))
|
||||
.$className
|
||||
.$methodName;
|
||||
}
|
||||
}
|
||||
136
vendor/knuckleswtf/scribe/src/Extracting/Shared/ApiResourceResponseTools.php
vendored
Normal file
136
vendor/knuckleswtf/scribe/src/Extracting/Shared/ApiResourceResponseTools.php
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Shared;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Pagination\CursorPaginator;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Tools\ErrorHandlingUtils as e;
|
||||
use Knuckles\Scribe\Tools\Utils;
|
||||
use Mpociot\Reflection\DocBlock;
|
||||
use Mpociot\Reflection\DocBlock\Tag;
|
||||
|
||||
class ApiResourceResponseTools
|
||||
{
|
||||
public static function fetch(
|
||||
string $apiResourceClass,
|
||||
bool $isCollection,
|
||||
?callable $modelInstantiator,
|
||||
ExtractedEndpointData $endpointData,
|
||||
array $pagination,
|
||||
array $additionalData,
|
||||
) {
|
||||
$resource = static::getApiResourceOrCollectionInstance(
|
||||
$apiResourceClass,
|
||||
$isCollection,
|
||||
$modelInstantiator,
|
||||
$pagination,
|
||||
$additionalData
|
||||
);
|
||||
$response = static::callApiResourceAndGetResponse($resource, $endpointData);
|
||||
|
||||
return $response->getContent();
|
||||
}
|
||||
|
||||
public static function callApiResourceAndGetResponse(JsonResource $resource, ExtractedEndpointData $endpointData): JsonResponse
|
||||
{
|
||||
$uri = Utils::getUrlWithBoundParameters($endpointData->route->uri(), $endpointData->cleanUrlParameters);
|
||||
$method = $endpointData->route->methods()[0];
|
||||
$request = Request::create($uri, $method);
|
||||
$request->headers->add(['Accept' => 'application/json']);
|
||||
// Set the route properly, so it works for users who have code that checks for the route.
|
||||
$request->setRouteResolver(fn () => $endpointData->route);
|
||||
|
||||
$previousBoundRequest = app('request');
|
||||
app()->bind('request', fn () => $request);
|
||||
|
||||
$response = $resource->toResponse($request);
|
||||
|
||||
app()->bind('request', fn () => $previousBoundRequest);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public static function getApiResourceOrCollectionInstance(
|
||||
string $apiResourceClass,
|
||||
bool $isCollection,
|
||||
?callable $modelInstantiator,
|
||||
array $paginationStrategy = [],
|
||||
array $additionalData = [],
|
||||
): JsonResource {
|
||||
// If the API Resource uses an empty $resource (e.g. an empty array), the $modelInstantiator will be null
|
||||
// See https://github.com/knuckleswtf/scribe/issues/652
|
||||
$modelInstance = is_callable($modelInstantiator) ? $modelInstantiator() : [];
|
||||
|
||||
try {
|
||||
$resource = new $apiResourceClass($modelInstance);
|
||||
} catch (\Exception) {
|
||||
// If it is a ResourceCollection class, it might throw an error
|
||||
// when trying to instantiate with something other than a collection
|
||||
$resource = new $apiResourceClass(collect([$modelInstance]));
|
||||
}
|
||||
|
||||
if ($isCollection) {
|
||||
// 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, $modelInstantiator()];
|
||||
// Pagination can be in two forms:
|
||||
// [15] : means ::paginate(15)
|
||||
// [15, 'simple'] : means ::simplePaginate(15)
|
||||
if (count($paginationStrategy) === 1) {
|
||||
$perPage = $paginationStrategy[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;
|
||||
} elseif (count($paginationStrategy) === 2 && $paginationStrategy[1] === 'simple') {
|
||||
$perPage = $paginationStrategy[0];
|
||||
$paginator = new Paginator($models, $perPage);
|
||||
$list = $paginator;
|
||||
} elseif (count($paginationStrategy) === 2 && $paginationStrategy[1] === 'cursor') {
|
||||
$perPage = $paginationStrategy[0];
|
||||
$paginator = new CursorPaginator($models, $perPage);
|
||||
$list = $paginator;
|
||||
} else {
|
||||
$list = collect($models);
|
||||
}
|
||||
|
||||
/** @var JsonResource $resource */
|
||||
$resource = $resource instanceof ResourceCollection
|
||||
? new $apiResourceClass($list) : $apiResourceClass::collection($list);
|
||||
}
|
||||
|
||||
return $resource->additional($additionalData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the ApiResource class has an `@mixin` docblock, and fetch the model from there.
|
||||
*/
|
||||
public static function tryToInferApiResourceModel(string $apiResourceClass): ?string
|
||||
{
|
||||
$class = new \ReflectionClass($apiResourceClass);
|
||||
$docBlock = new DocBlock($class->getDocComment() ?: '');
|
||||
|
||||
/** @var null|Tag $mixinTag */
|
||||
$mixinTag = Arr::first(Utils::filterDocBlockTags($docBlock->getTags(), 'mixin'));
|
||||
if (empty($mixinTag) || empty($modelClass = mb_trim($mixinTag->getContent()))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (class_exists($modelClass)) {
|
||||
return $modelClass;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
38
vendor/knuckleswtf/scribe/src/Extracting/Shared/ResponseFieldTools.php
vendored
Normal file
38
vendor/knuckleswtf/scribe/src/Extracting/Shared/ResponseFieldTools.php
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Shared;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Camel\Extraction\Response;
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
|
||||
class ResponseFieldTools
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
public static function inferTypeOfResponseField(array $data, ExtractedEndpointData $endpointData): string
|
||||
{
|
||||
if (! empty($data['type'])) {
|
||||
return self::normalizeTypeName($data['type']);
|
||||
}
|
||||
|
||||
// Try to get a type from first 2xx response
|
||||
$validResponse = collect($endpointData->responses)->first(
|
||||
fn (Response $r) => $r->status >= 200 && $r->status < 300
|
||||
);
|
||||
if ($validResponse && ($validResponseContent = json_decode($validResponse->content, true))) {
|
||||
$nonexistent = new \stdClass;
|
||||
$value = $validResponseContent[$data['name']]
|
||||
?? $validResponseContent['data'][$data['name']] // Maybe it's a Laravel ApiResource
|
||||
?? $validResponseContent[0][$data['name']] // Maybe it's a list
|
||||
?? $validResponseContent['data'][0][$data['name']] // Maybe an Api Resource Collection?
|
||||
?? $nonexistent;
|
||||
|
||||
if ($value !== $nonexistent) {
|
||||
return self::normalizeTypeName(gettype($value), $value);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
36
vendor/knuckleswtf/scribe/src/Extracting/Shared/ResponseFileTools.php
vendored
Normal file
36
vendor/knuckleswtf/scribe/src/Extracting/Shared/ResponseFileTools.php
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Shared;
|
||||
|
||||
class ResponseFileTools
|
||||
{
|
||||
public static function getResponseContents($filePath, array|string|null $merge): string
|
||||
{
|
||||
$content = self::getFileContents($filePath);
|
||||
if (empty($merge)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if (is_string($merge)) {
|
||||
$json = str_replace("'", '"', $merge);
|
||||
|
||||
return json_encode(array_merge(json_decode($content, true), json_decode($json, true)));
|
||||
}
|
||||
|
||||
return json_encode(array_merge(json_decode($content, true), $merge));
|
||||
}
|
||||
|
||||
protected static function getFileContents($filePath): string
|
||||
{
|
||||
if (! file_exists($filePath)) {
|
||||
// Try Laravel storage folder
|
||||
if (! file_exists(storage_path($filePath))) {
|
||||
throw new \InvalidArgumentException("@responseFile {$filePath} does not exist");
|
||||
}
|
||||
|
||||
$filePath = storage_path($filePath);
|
||||
}
|
||||
|
||||
return file_get_contents($filePath, true);
|
||||
}
|
||||
}
|
||||
40
vendor/knuckleswtf/scribe/src/Extracting/Shared/TransformerResponseTools.php
vendored
Normal file
40
vendor/knuckleswtf/scribe/src/Extracting/Shared/TransformerResponseTools.php
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Shared;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use League\Fractal\Manager;
|
||||
use League\Fractal\Resource\Collection;
|
||||
use League\Fractal\Resource\Item;
|
||||
|
||||
class TransformerResponseTools
|
||||
{
|
||||
public static function fetch(string $transformerClass, bool $isCollection, $modelInstantiator, array $pagination = [], ?string $resourceKey = null, ?string $serializer = null)
|
||||
{
|
||||
$fractal = new Manager;
|
||||
|
||||
if (! is_null($serializer)) {
|
||||
$fractal->setSerializer(app($serializer));
|
||||
}
|
||||
|
||||
$modelInstance = $modelInstantiator();
|
||||
if ($isCollection) {
|
||||
$models = [$modelInstance, $modelInstantiator()];
|
||||
$resource = new Collection($models, new $transformerClass);
|
||||
|
||||
['adapter' => $paginatorAdapter, 'perPage' => $perPage] = $pagination;
|
||||
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 $transformerClass, $resourceKey);
|
||||
$paginator = new LengthAwarePaginator($firstPage, $total, $perPage);
|
||||
$resource->setPaginator(new $paginatorAdapter($paginator));
|
||||
}
|
||||
} else {
|
||||
$resource = (new Item($modelInstance, new $transformerClass, $resourceKey));
|
||||
}
|
||||
|
||||
return response($fractal->createData($resource)->toJson())->getContent();
|
||||
}
|
||||
}
|
||||
226
vendor/knuckleswtf/scribe/src/Extracting/Shared/UrlParamsNormalizer.php
vendored
Normal file
226
vendor/knuckleswtf/scribe/src/Extracting/Shared/UrlParamsNormalizer.php
vendored
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Shared;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
// See https://laravel.com/docs/9.x/routing#route-model-binding
|
||||
class UrlParamsNormalizer
|
||||
{
|
||||
/**
|
||||
* Normalize a URL from Laravel-style to something that's clearer for a non-Laravel user.
|
||||
* For instance, `/posts/{post}` would be clearer as `/posts/{id}`,
|
||||
* and `/users/{user}/posts/{post}` would be clearer as `/users/{user_id}/posts/{id}`.
|
||||
*/
|
||||
public static function normalizeParameterNamesInRouteUri(Route $route, \ReflectionFunctionAbstract $method): string
|
||||
{
|
||||
$params = [];
|
||||
$uri = $route->uri;
|
||||
preg_match_all('#\{(\w+?)}#', $uri, $params);
|
||||
|
||||
$resourceRouteNames = ['.index', '.show', '.update', '.destroy', '.store'];
|
||||
|
||||
$typeHintedEloquentModels = self::getTypeHintedEloquentModels($method);
|
||||
$routeName = $route->action['as'] ?? '';
|
||||
if (Str::endsWith($routeName, $resourceRouteNames)) {
|
||||
// Note that resource routes can be nested eg users.posts.show
|
||||
$pluralResources = explode('.', $routeName);
|
||||
array_pop($pluralResources); // Remove the name of the action (eg `show`)
|
||||
|
||||
$alreadyFoundResourceParam = false;
|
||||
foreach (array_reverse($pluralResources) as $pluralResource) {
|
||||
$singularResource = Str::singular($pluralResource);
|
||||
|
||||
// Laravel turns hyphens in parameters to underscores
|
||||
// (`cool-things/{cool-thing}` to `cool-things/{cool_thing_id}`)
|
||||
// so we do the same
|
||||
$singularResourceParam = str_replace('-', '_', $singularResource);
|
||||
|
||||
$urlPatternsToSearchFor = [
|
||||
"{$pluralResource}/{{$singularResourceParam}}",
|
||||
"{$pluralResource}/{{$singularResource}}",
|
||||
"{$pluralResource}/{{$singularResourceParam}?}",
|
||||
"{$pluralResource}/{{$singularResource}?}",
|
||||
];
|
||||
|
||||
$binding = self::getRouteKeyForUrlParam(
|
||||
$route,
|
||||
$singularResource,
|
||||
$typeHintedEloquentModels,
|
||||
'id'
|
||||
);
|
||||
|
||||
if (! $alreadyFoundResourceParam) {
|
||||
// This is the first resource param (from the end).
|
||||
// We set it to `params/{id}` (or whatever field it's bound to)
|
||||
$replaceWith = [
|
||||
"{$pluralResource}/{{$binding}}",
|
||||
"{$pluralResource}/{{$binding}}",
|
||||
"{$pluralResource}/{{$binding}?}",
|
||||
"{$pluralResource}/{{$binding}?}",
|
||||
];
|
||||
$alreadyFoundResourceParam = true;
|
||||
} else {
|
||||
// Other resource parameters will be `params/{<param>_id}`
|
||||
$replaceWith = [
|
||||
"{$pluralResource}/{{$singularResourceParam}_{$binding}}",
|
||||
"{$pluralResource}/{{$singularResource}_{$binding}}",
|
||||
"{$pluralResource}/{{$singularResourceParam}_{$binding}?}",
|
||||
"{$pluralResource}/{{$singularResource}_{$binding}?}",
|
||||
];
|
||||
}
|
||||
$uri = str_replace($urlPatternsToSearchFor, $replaceWith, $uri);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($params[1] as $param) {
|
||||
// For non-resource parameters, if there's a field binding/type-hinted variable, replace that too:
|
||||
if ($binding = self::getRouteKeyForUrlParam($route, $param, $typeHintedEloquentModels)) {
|
||||
$urlPatternsToSearchFor = ["{{$param}}", "{{$param}?}"];
|
||||
$replaceWith = ["{{$param}_{$binding}}", "{{$param}_{$binding}?}"];
|
||||
$uri = str_replace($urlPatternsToSearchFor, $replaceWith, $uri);
|
||||
}
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type-hinted method arguments in the action that are Eloquent models,
|
||||
* The arguments will be returned as an array of the form: [<variable_name> => $instance].
|
||||
*/
|
||||
public static function getTypeHintedEloquentModels(\ReflectionFunctionAbstract $method): array
|
||||
{
|
||||
$arguments = [];
|
||||
foreach ($method->getParameters() as $argument) {
|
||||
if (($instance = self::instantiateMethodArgument($argument)) && $instance instanceof Model) {
|
||||
$arguments[$argument->getName()] = $instance;
|
||||
}
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type-hinted method arguments in the action that are enums,
|
||||
* The arguments will be returned as an array of the form: [<variable_name> => $instance].
|
||||
*/
|
||||
public static function getTypeHintedEnums(\ReflectionFunctionAbstract $method): array
|
||||
{
|
||||
$arguments = [];
|
||||
foreach ($method->getParameters() as $argument) {
|
||||
$argumentType = $argument->getType();
|
||||
if (! $argumentType instanceof \ReflectionNamedType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$reflectionEnum = new \ReflectionEnum($argumentType->getName());
|
||||
$arguments[$argument->getName()] = $reflectionEnum;
|
||||
} catch (\ReflectionException) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL that uses Eloquent model binding (for instance `/posts/{post}` -> `public function show(Post
|
||||
* $post)`), we need to figure out the field that Eloquent uses to retrieve the Post object. By default, this would
|
||||
* be `id`, but can be configured in a couple of ways:
|
||||
*
|
||||
* - Inline: `/posts/{post:slug}`
|
||||
* - `class Post { public function getRouteKeyName() { return 'slug'; } }`
|
||||
*
|
||||
* There are other ways, but they're dynamic and beyond our scope.
|
||||
*
|
||||
* @param string $paramName The name of the URL parameter
|
||||
* @param array<string, Model> $typeHintedEloquentModels
|
||||
* @param null|string $default Default field to use
|
||||
*/
|
||||
protected static function getRouteKeyForUrlParam(
|
||||
Route $route,
|
||||
string $paramName,
|
||||
array $typeHintedEloquentModels = [],
|
||||
?string $default = null,
|
||||
): ?string {
|
||||
if ($binding = self::getInlineRouteKey($route, $paramName)) {
|
||||
return $binding;
|
||||
}
|
||||
|
||||
return self::getRouteKeyFromModel($paramName, $typeHintedEloquentModels) ?: $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the `slug` in /posts/{post:slug}.
|
||||
*/
|
||||
protected static function getInlineRouteKey(Route $route, string $paramName): ?string
|
||||
{
|
||||
return $route->bindingFieldFor($paramName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's a type-hinted argument on the controller method matching the URL param name:
|
||||
* eg /posts/{post} -> public function show(Post $post)
|
||||
* If there is, check if it's an Eloquent model.
|
||||
* If it is, return it's `getRouteKeyName()`.
|
||||
*
|
||||
* @param Model[] $typeHintedEloquentModels
|
||||
*/
|
||||
protected static function getRouteKeyFromModel(string $paramName, array $typeHintedEloquentModels): ?string
|
||||
{
|
||||
// Ensure param name is in camelCase so it matches the argument name (e.g. The '$userAddress' in `function show(BigThing $userAddress`)
|
||||
$paramName = Str::camel($paramName);
|
||||
|
||||
if (array_key_exists($paramName, $typeHintedEloquentModels)) {
|
||||
$argumentInstance = $typeHintedEloquentModels[$paramName];
|
||||
|
||||
return $argumentInstance->getRouteKeyName();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate an argument on a controller method via its typehint. For instance, $post in:
|
||||
*
|
||||
* public function show(Post $post)
|
||||
*
|
||||
* This method takes in a method argument and returns an instance, or null if it couldn't be instantiated safely.
|
||||
* Cases where instantiation may fail:
|
||||
* - the argument has no type (eg `public function show($postId)`)
|
||||
* - the argument has a primitive type (eg `public function show(string $postId)`)
|
||||
* - the argument is an injected dependency that itself needs other dependencies
|
||||
* (eg `public function show(PostsManager $manager)`)
|
||||
*/
|
||||
protected static function instantiateMethodArgument(\ReflectionParameter $argument): ?object
|
||||
{
|
||||
$argumentType = $argument->getType();
|
||||
// No type-hint, or primitive type
|
||||
if (! $argumentType instanceof \ReflectionNamedType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$argumentClassName = $argumentType->getName();
|
||||
if (class_exists($argumentClassName)) {
|
||||
try {
|
||||
return new $argumentClassName;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (interface_exists($argumentClassName)) {
|
||||
try {
|
||||
return app($argumentClassName);
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
43
vendor/knuckleswtf/scribe/src/Extracting/Shared/ValidationRulesFinders/RequestValidate.php
vendored
Normal file
43
vendor/knuckleswtf/scribe/src/Extracting/Shared/ValidationRulesFinders/RequestValidate.php
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders;
|
||||
|
||||
use PhpParser\Node;
|
||||
|
||||
/**
|
||||
* This class looks for
|
||||
* $anyVariable = $request->validate(...);
|
||||
* or just
|
||||
* $request->validate(...);.
|
||||
*
|
||||
* Also supports `$req` instead of `$request`
|
||||
* Also supports `->validateWithBag('', ...)`
|
||||
*/
|
||||
class RequestValidate
|
||||
{
|
||||
public static function find(Node $node)
|
||||
{
|
||||
if (! $node instanceof Node\Stmt\Expression) {
|
||||
return;
|
||||
}
|
||||
|
||||
$expr = $node->expr;
|
||||
if ($expr instanceof Node\Expr\Assign) {
|
||||
$expr = $expr->expr; // If it's an assignment, get the expression on the RHS
|
||||
}
|
||||
|
||||
if (
|
||||
$expr instanceof Node\Expr\MethodCall
|
||||
&& $expr->var instanceof Node\Expr\Variable
|
||||
&& in_array($expr->var->name, ['request', 'req'])
|
||||
) {
|
||||
if ($expr->name->name === 'validate') {
|
||||
return $expr->args[0]->value;
|
||||
}
|
||||
|
||||
if ($expr->name->name === 'validateWithBag') {
|
||||
return $expr->args[1]->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
vendor/knuckleswtf/scribe/src/Extracting/Shared/ValidationRulesFinders/RequestValidateFacade.php
vendored
Normal file
43
vendor/knuckleswtf/scribe/src/Extracting/Shared/ValidationRulesFinders/RequestValidateFacade.php
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders;
|
||||
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use PhpParser\Node;
|
||||
|
||||
/**
|
||||
* This class looks for
|
||||
* $anyVariable = Request::validate(...);
|
||||
* or just
|
||||
* Request::validate(...);.
|
||||
*
|
||||
* Also supports `->validateWithBag('', ...)`
|
||||
*/
|
||||
class RequestValidateFacade
|
||||
{
|
||||
public static function find(Node $node)
|
||||
{
|
||||
if (! $node instanceof Node\Stmt\Expression) {
|
||||
return;
|
||||
}
|
||||
|
||||
$expr = $node->expr;
|
||||
if ($expr instanceof Node\Expr\Assign) {
|
||||
$expr = $expr->expr; // If it's an assignment, get the expression on the RHS
|
||||
}
|
||||
|
||||
if (
|
||||
$expr instanceof Node\Expr\StaticCall
|
||||
&& $expr->class instanceof Node\Name
|
||||
&& in_array($expr->class->name, ['Request', Request::class])
|
||||
) {
|
||||
if ($expr->name->name === 'validate') {
|
||||
return $expr->args[0]->value;
|
||||
}
|
||||
|
||||
if ($expr->name->name === 'validateWithBag') {
|
||||
return $expr->args[1]->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
vendor/knuckleswtf/scribe/src/Extracting/Shared/ValidationRulesFinders/ThisValidate.php
vendored
Normal file
38
vendor/knuckleswtf/scribe/src/Extracting/Shared/ValidationRulesFinders/ThisValidate.php
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders;
|
||||
|
||||
use PhpParser\Node;
|
||||
|
||||
/**
|
||||
* This class looks for
|
||||
* $anyVariable = $this->validate($request, ...);
|
||||
* or just
|
||||
* $this->validate($request, ...);.
|
||||
*
|
||||
* Also supports `$req` instead of `$request`
|
||||
*/
|
||||
class ThisValidate
|
||||
{
|
||||
public static function find(Node $node)
|
||||
{
|
||||
if (! $node instanceof Node\Stmt\Expression) {
|
||||
return;
|
||||
}
|
||||
|
||||
$expr = $node->expr;
|
||||
if ($expr instanceof Node\Expr\Assign) {
|
||||
$expr = $expr->expr; // If it's an assignment, get the expression on the RHS
|
||||
}
|
||||
|
||||
if (
|
||||
$expr instanceof Node\Expr\MethodCall
|
||||
&& $expr->var instanceof Node\Expr\Variable
|
||||
&& $expr->var->name === 'this'
|
||||
) {
|
||||
if ($expr->name->name === 'validate') {
|
||||
return $expr->args[1]->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
vendor/knuckleswtf/scribe/src/Extracting/Shared/ValidationRulesFinders/ValidatorMake.php
vendored
Normal file
35
vendor/knuckleswtf/scribe/src/Extracting/Shared/ValidationRulesFinders/ValidatorMake.php
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeFinder;
|
||||
|
||||
/**
|
||||
* This class looks for
|
||||
* $validator = Validator::make($request, ...)
|
||||
* Validator::make($request, ...)->validate().
|
||||
*
|
||||
* The variable names (`$validator` and `$request`) don't matter.
|
||||
*/
|
||||
class ValidatorMake
|
||||
{
|
||||
public static function find(Node $node)
|
||||
{
|
||||
// Make sure it's an assignment
|
||||
if (! $node instanceof Node\Stmt\Expression) {
|
||||
return;
|
||||
}
|
||||
|
||||
$validatorNode = (new NodeFinder)->findFirst($node, function ($node): bool {
|
||||
return $node instanceof Node\Expr\StaticCall
|
||||
&& ! empty($node->class->name)
|
||||
&& str_ends_with($node->class->name, 'Validator')
|
||||
&& $node->name->name === 'make';
|
||||
});
|
||||
|
||||
if ($validatorNode instanceof Node\Expr\StaticCall) {
|
||||
return $validatorNode->args[1]->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
vendor/knuckleswtf/scribe/src/Extracting/Strategies/BodyParameters/GetFromBodyParamAttribute.php
vendored
Normal file
14
vendor/knuckleswtf/scribe/src/Extracting/Strategies/BodyParameters/GetFromBodyParamAttribute.php
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\BodyParameters;
|
||||
|
||||
use Knuckles\Scribe\Attributes\BodyParam;
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetParamsFromAttributeStrategy;
|
||||
|
||||
/**
|
||||
* @extends GetParamsFromAttributeStrategy<BodyParam>
|
||||
*/
|
||||
class GetFromBodyParamAttribute extends GetParamsFromAttributeStrategy
|
||||
{
|
||||
protected static array $attributeNames = [BodyParam::class];
|
||||
}
|
||||
47
vendor/knuckleswtf/scribe/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php
vendored
Normal file
47
vendor/knuckleswtf/scribe/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\BodyParameters;
|
||||
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetFieldsFromTagStrategy;
|
||||
|
||||
class GetFromBodyParamTag extends GetFieldsFromTagStrategy
|
||||
{
|
||||
protected string $tagName = 'bodyParam';
|
||||
|
||||
public function parseTag(string $tagContent): array
|
||||
{
|
||||
// Format:
|
||||
// @bodyParam <name> <type> <"required" (optional)> <"deprecated" (optional)> <description>
|
||||
// Examples:
|
||||
// @bodyParam text string required The text.
|
||||
// @bodyParam user_id integer The ID of the user.
|
||||
// @bodyParam status string required deprecated Use `is_active` instead.
|
||||
preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(deprecated\s+)?([\s\S]*)/', $tagContent, $parsedContent);
|
||||
|
||||
if (empty($parsedContent)) {
|
||||
// This means only name and type were supplied
|
||||
[$name, $type] = preg_split('/\s+/', $tagContent);
|
||||
$required = false;
|
||||
$deprecated = false;
|
||||
$description = '';
|
||||
} else {
|
||||
[$_, $name, $type, $required, $deprecated, $description] = $parsedContent;
|
||||
$description = mb_trim(str_replace(['No-example.', 'No-example'], '', $description));
|
||||
if ($description === 'required') {
|
||||
$required = $description;
|
||||
$description = '';
|
||||
} elseif ($description === 'deprecated') {
|
||||
$deprecated = $description;
|
||||
$description = '';
|
||||
}
|
||||
$required = mb_trim($required) === 'required';
|
||||
$deprecated = mb_trim($deprecated) === 'deprecated';
|
||||
}
|
||||
|
||||
$type = static::normalizeTypeName($type);
|
||||
[$description, $example, $enumValues, $exampleWasSpecified]
|
||||
= $this->getDescriptionAndExample($description, $type, $tagContent, $name);
|
||||
|
||||
return compact('name', 'type', 'description', 'required', 'deprecated', 'example', 'enumValues', 'exampleWasSpecified');
|
||||
}
|
||||
}
|
||||
23
vendor/knuckleswtf/scribe/src/Extracting/Strategies/BodyParameters/GetFromFormRequest.php
vendored
Normal file
23
vendor/knuckleswtf/scribe/src/Extracting/Strategies/BodyParameters/GetFromFormRequest.php
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\BodyParameters;
|
||||
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetFromFormRequestBase;
|
||||
|
||||
class GetFromFormRequest extends GetFromFormRequestBase
|
||||
{
|
||||
protected string $customParameterDataMethodName = 'bodyParameters';
|
||||
|
||||
protected function isFormRequestMeantForThisStrategy(\ReflectionClass $formRequestReflectionClass): bool
|
||||
{
|
||||
// Only use this FormRequest for body params if there's no "Query parameters" in the docblock
|
||||
// Or there's a bodyParameters() method
|
||||
$formRequestDocBlock = $formRequestReflectionClass->getDocComment();
|
||||
if (mb_strpos(mb_strtolower($formRequestDocBlock), 'query parameters') !== false
|
||||
|| $formRequestReflectionClass->hasMethod('queryParameters')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
21
vendor/knuckleswtf/scribe/src/Extracting/Strategies/BodyParameters/GetFromInlineValidator.php
vendored
Normal file
21
vendor/knuckleswtf/scribe/src/Extracting/Strategies/BodyParameters/GetFromInlineValidator.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\BodyParameters;
|
||||
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetFromInlineValidatorBase;
|
||||
use PhpParser\Node;
|
||||
|
||||
class GetFromInlineValidator extends GetFromInlineValidatorBase
|
||||
{
|
||||
protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool
|
||||
{
|
||||
// Only use this validator for body params if there's no "// Query parameters" comment above
|
||||
$comments = $validationStatement->getComments();
|
||||
$comments = implode("\n", array_map(fn ($comment) => $comment->getReformattedText(), $comments));
|
||||
if (mb_strpos(mb_strtolower($comments), 'query parameters') !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
68
vendor/knuckleswtf/scribe/src/Extracting/Strategies/GetFieldsFromTagStrategy.php
vendored
Normal file
68
vendor/knuckleswtf/scribe/src/Extracting/Strategies/GetFieldsFromTagStrategy.php
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
|
||||
abstract class GetFieldsFromTagStrategy extends TagStrategyWithFormRequestFallback
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
protected string $tagName = '';
|
||||
|
||||
public function getFromTags(array $tagsOnMethod, array $tagsOnClass = []): array
|
||||
{
|
||||
$fields = [];
|
||||
|
||||
foreach ($tagsOnClass as $tag) {
|
||||
if (mb_strtolower($tag->getName()) !== mb_strtolower($this->tagName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldData = $this->parseTag(mb_trim($tag->getContent()));
|
||||
$fields[$fieldData['name']] = $fieldData;
|
||||
}
|
||||
|
||||
foreach ($tagsOnMethod as $tag) {
|
||||
if (mb_strtolower($tag->getName()) !== mb_strtolower($this->tagName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldData = $this->parseTag(mb_trim($tag->getContent()));
|
||||
$fields[$fieldData['name']] = $fieldData;
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
abstract protected function parseTag(string $tagContent): array;
|
||||
|
||||
protected function getDescriptionAndExample(
|
||||
string $description,
|
||||
string $type,
|
||||
string $tagContent,
|
||||
string $fieldName,
|
||||
): array {
|
||||
[$description, $example, $enumValues, $exampleWasSpecified] = $this->parseExampleFromParamDescription($description, $type);
|
||||
|
||||
if ($exampleWasSpecified && $example === null) {
|
||||
$example = null;
|
||||
} else {
|
||||
$example = $this->setExampleIfNeeded($example, $type, $tagContent, $fieldName, $enumValues);
|
||||
}
|
||||
|
||||
return [$description, $example, $enumValues, $exampleWasSpecified];
|
||||
}
|
||||
|
||||
protected function setExampleIfNeeded(
|
||||
mixed $currentExample,
|
||||
string $type,
|
||||
string $tagContent,
|
||||
string $fieldName,
|
||||
?array $enumValues = [],
|
||||
): mixed {
|
||||
return (is_null($currentExample) && ! $this->shouldExcludeExample($tagContent))
|
||||
? $this->generateDummyValue($type, hints: ['name' => $fieldName, 'enumValues' => $enumValues])
|
||||
: $currentExample;
|
||||
}
|
||||
}
|
||||
98
vendor/knuckleswtf/scribe/src/Extracting/Strategies/GetFromFormRequestBase.php
vendored
Normal file
98
vendor/knuckleswtf/scribe/src/Extracting/Strategies/GetFromFormRequestBase.php
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Routing\Route;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\FindsFormRequestForMethod;
|
||||
use Knuckles\Scribe\Extracting\ParsesValidationRules;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\Globals;
|
||||
|
||||
class GetFromFormRequestBase extends Strategy
|
||||
{
|
||||
use FindsFormRequestForMethod;
|
||||
use ParsesValidationRules;
|
||||
|
||||
protected string $customParameterDataMethodName = '';
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
|
||||
{
|
||||
return $this->getParametersFromFormRequest($endpointData->method, $endpointData->route);
|
||||
}
|
||||
|
||||
public function getParametersFromFormRequest(\ReflectionFunctionAbstract $method, Route $route): array
|
||||
{
|
||||
if (! $formRequestReflectionClass = $this->getFormRequestReflectionClass($method)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! $this->isFormRequestMeantForThisStrategy($formRequestReflectionClass)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$className = $formRequestReflectionClass->getName();
|
||||
|
||||
if (Globals::$__instantiateFormRequestUsing) {
|
||||
$formRequest = call_user_func_array(Globals::$__instantiateFormRequestUsing, [$className, $route, $method]);
|
||||
} else {
|
||||
$formRequest = new $className;
|
||||
}
|
||||
// Set the route properly so it works for users who have code that checks for the route.
|
||||
/** @var FormRequest $formRequest */
|
||||
$formRequest->setRouteResolver(function () use ($formRequest, $route) {
|
||||
// Also need to bind the request to the route in case their code tries to inspect current request
|
||||
return $route->bind($formRequest);
|
||||
});
|
||||
$formRequest->server->set('REQUEST_METHOD', $route->methods()[0]);
|
||||
|
||||
$parametersFromFormRequest = $this->getParametersFromValidationRules(
|
||||
$this->getRouteValidationRules($formRequest),
|
||||
$this->getCustomParameterData($formRequest)
|
||||
);
|
||||
|
||||
return $this->normaliseArrayAndObjectParameters($parametersFromFormRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getRouteValidationRules(FormRequest $formRequest)
|
||||
{
|
||||
if (method_exists($formRequest, 'validator')) {
|
||||
$validationFactory = app(ValidationFactory::class);
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
return app()->call([$formRequest, 'validator'], [$validationFactory])
|
||||
->getRules();
|
||||
}
|
||||
if (method_exists($formRequest, 'rules')) {
|
||||
return app()->call([$formRequest, 'rules']);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getCustomParameterData(FormRequest $formRequest)
|
||||
{
|
||||
if (method_exists($formRequest, $this->customParameterDataMethodName)) {
|
||||
return call_user_func_array([$formRequest, $this->customParameterDataMethodName], []);
|
||||
}
|
||||
|
||||
c::warn("No {$this->customParameterDataMethodName}() method found in ".get_class($formRequest).'. Scribe will only be able to extract basic information from the rules() method.');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getMissingCustomDataMessage($parameterName)
|
||||
{
|
||||
return "No data found for parameter '{$parameterName}' in your {$this->customParameterDataMethodName}() method. Add an entry for '{$parameterName}' so you can add a description and example.";
|
||||
}
|
||||
|
||||
protected function isFormRequestMeantForThisStrategy(\ReflectionClass $formRequestReflectionClass): bool
|
||||
{
|
||||
return $formRequestReflectionClass->hasMethod($this->customParameterDataMethodName);
|
||||
}
|
||||
}
|
||||
207
vendor/knuckleswtf/scribe/src/Extracting/Strategies/GetFromInlineValidatorBase.php
vendored
Normal file
207
vendor/knuckleswtf/scribe/src/Extracting/Strategies/GetFromInlineValidatorBase.php
vendored
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\MethodAstParser;
|
||||
use Knuckles\Scribe\Extracting\ParsesValidationRules;
|
||||
use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\RequestValidate;
|
||||
use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\RequestValidateFacade;
|
||||
use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\ThisValidate;
|
||||
use Knuckles\Scribe\Extracting\Shared\ValidationRulesFinders\ValidatorMake;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\ClassMethod;
|
||||
|
||||
class GetFromInlineValidatorBase extends Strategy
|
||||
{
|
||||
use ParsesValidationRules;
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
|
||||
{
|
||||
if (! $endpointData->method instanceof \ReflectionMethod) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$methodAst = MethodAstParser::getMethodAst($endpointData->method);
|
||||
[$validationRules, $customParameterData] = $this->lookForInlineValidationRules($methodAst);
|
||||
|
||||
$bodyParametersFromValidationRules = $this->getParametersFromValidationRules($validationRules, $customParameterData);
|
||||
|
||||
return $this->normaliseArrayAndObjectParameters($bodyParametersFromValidationRules);
|
||||
}
|
||||
|
||||
public function lookForInlineValidationRules(ClassMethod $methodAst): array
|
||||
{
|
||||
// Validation usually happens early on, so let's assume it's in the first 10 statements
|
||||
$statements = array_slice($methodAst->stmts, 0, 10);
|
||||
|
||||
[$index, $validationStatement, $validationRules] = $this->findValidationExpression($statements);
|
||||
|
||||
if ($validationStatement
|
||||
&& ! $this->isValidationStatementMeantForThisStrategy($validationStatement)) {
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
// If validation rules were saved in a variable (like $rules),
|
||||
// try to find the var and expand the value
|
||||
if ($validationRules instanceof Node\Expr\Variable) {
|
||||
foreach (array_reverse(array_slice($statements, 0, $index)) as $earlierStatement) {
|
||||
if (
|
||||
$earlierStatement instanceof Node\Stmt\Expression
|
||||
&& $earlierStatement->expr instanceof Node\Expr\Assign
|
||||
&& $earlierStatement->expr->var instanceof Node\Expr\Variable
|
||||
&& $earlierStatement->expr->var->name === $validationRules->name
|
||||
) {
|
||||
$validationRules = $earlierStatement->expr->expr;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $validationRules instanceof Node\Expr\Array_) {
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
$rules = [];
|
||||
$customParameterData = [];
|
||||
foreach ($validationRules->items as $item) {
|
||||
/** @var Node\ArrayItem $item */
|
||||
if (! $item->key instanceof Node\Scalar\String_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$paramName = $item->key->value;
|
||||
|
||||
// Might be an expression or concatenated string, etc.
|
||||
// For now, let's focus on simple strings and arrays of strings
|
||||
if ($item->value instanceof Node\Scalar\String_) {
|
||||
$rules[$paramName] = $item->value->value;
|
||||
} elseif ($item->value instanceof Node\Expr\Array_) {
|
||||
$rulesList = [];
|
||||
foreach ($item->value->items as $arrayItem) {
|
||||
/** @var Node\ArrayItem $arrayItem */
|
||||
if ($arrayItem->value instanceof Node\Scalar\String_) {
|
||||
$rulesList[] = $arrayItem->value->value;
|
||||
}
|
||||
// Try to extract Enum rule
|
||||
elseif (
|
||||
($enum = $this->extractEnumClassFromArrayItem($arrayItem))
|
||||
&& enum_exists($enum) && method_exists($enum, 'tryFrom')
|
||||
) {
|
||||
// $case->value only exists on BackedEnums, not UnitEnums
|
||||
// method_exists($enum, 'tryFrom') implies the enum is a BackedEnum
|
||||
// @phpstan-ignore-next-line
|
||||
$rulesList[] = 'in:'.implode(',', array_map(fn ($case) => $case->value, $enum::cases()));
|
||||
}
|
||||
}
|
||||
$rules[$paramName] = implode('|', $rulesList);
|
||||
} else {
|
||||
$rules[$paramName] = [];
|
||||
}
|
||||
|
||||
$dataFromComment = [];
|
||||
$comments = implode("\n", array_map(
|
||||
fn ($comment) => mb_ltrim(mb_ltrim($comment->getReformattedText(), '/')),
|
||||
$item->getComments()
|
||||
));
|
||||
|
||||
if ($comments) {
|
||||
if (str_contains($comments, 'No-example')) {
|
||||
$dataFromComment['example'] = null;
|
||||
}
|
||||
|
||||
$dataFromComment['description'] = mb_trim(str_replace(['No-example.', 'No-example'], '', $comments));
|
||||
if (preg_match('/(.*\s+|^)Example:\s*([\s\S]+)\s*/s', $dataFromComment['description'], $matches)) {
|
||||
$dataFromComment['description'] = mb_trim($matches[1]);
|
||||
$dataFromComment['example'] = $matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
$customParameterData[$paramName] = $dataFromComment;
|
||||
}
|
||||
|
||||
return [$rules, $customParameterData];
|
||||
}
|
||||
|
||||
protected function extractEnumClassFromArrayItem(Node\ArrayItem $arrayItem): ?string
|
||||
{
|
||||
$args = [];
|
||||
|
||||
// Enum rule with the form "new Enum(...)"
|
||||
if ($arrayItem->value instanceof Node\Expr\New_
|
||||
&& $arrayItem->value->class instanceof Node\Name
|
||||
&& str_ends_with($arrayItem->value->class->name, 'Enum')
|
||||
) {
|
||||
$args = $arrayItem->value->args;
|
||||
}
|
||||
|
||||
// Enum rule with the form "Rule::enum(...)"
|
||||
elseif ($arrayItem->value instanceof Node\Expr\StaticCall
|
||||
&& $arrayItem->value->class instanceof Node\Name
|
||||
&& str_ends_with($arrayItem->value->class->name, 'Rule')
|
||||
&& $arrayItem->value->name instanceof Node\Identifier
|
||||
&& $arrayItem->value->name->name === 'enum'
|
||||
) {
|
||||
$args = $arrayItem->value->args;
|
||||
}
|
||||
|
||||
if (count($args) !== 1 || ! $args[0] instanceof Node\Arg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$arg = $args[0];
|
||||
if ($arg->value instanceof Node\Expr\ClassConstFetch
|
||||
&& $arg->value->class instanceof Node\Name
|
||||
) {
|
||||
$className = $arg->value->class->getAttribute('resolvedName');
|
||||
// Only prepend '\\' if the class name is already fully qualified (contains '\')
|
||||
// For relative names, return as-is and let enum_exists use autoloading to resolve.
|
||||
if (mb_strpos($className, '\\') !== false) {
|
||||
return '\\'.$className;
|
||||
}
|
||||
|
||||
return $className;
|
||||
}
|
||||
if ($arg->value instanceof Node\Scalar\String_) {
|
||||
return $arg->value->value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getMissingCustomDataMessage($parameterName)
|
||||
{
|
||||
return "No extra data found for parameter '{$parameterName}' from your inline validator. You can add a comment above '{$parameterName}' with a description and example.";
|
||||
}
|
||||
|
||||
protected function shouldCastUserExample()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function findValidationExpression($statements): ?array
|
||||
{
|
||||
$strategies = [
|
||||
RequestValidate::class, // $request->validate(...);
|
||||
RequestValidateFacade::class, // Request::validate(...);
|
||||
ValidatorMake::class, // Validator::make($request, ...)
|
||||
ThisValidate::class, // $this->validate(...);
|
||||
];
|
||||
|
||||
foreach ($statements as $index => $node) {
|
||||
foreach ($strategies as $strategy) {
|
||||
if ($validationRules = $strategy::find($node)) {
|
||||
return [$index, $node, $validationRules];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [null, null, null];
|
||||
}
|
||||
}
|
||||
52
vendor/knuckleswtf/scribe/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php
vendored
Normal file
52
vendor/knuckleswtf/scribe/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Attributes\GenericParam;
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
|
||||
/**
|
||||
* @template T of GenericParam
|
||||
*
|
||||
* @extends PhpAttributeStrategy<T>
|
||||
*/
|
||||
class GetParamsFromAttributeStrategy extends PhpAttributeStrategy
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
protected function extractFromAttributes(
|
||||
ExtractedEndpointData $endpointData,
|
||||
array $attributesOnMethod,
|
||||
array $attributesOnFormRequest = [],
|
||||
array $attributesOnController = [],
|
||||
): ?array {
|
||||
$parameters = [];
|
||||
foreach ([...$attributesOnController, ...$attributesOnFormRequest, ...$attributesOnMethod] as $attributeInstance) {
|
||||
$parameters[$attributeInstance->name] = $attributeInstance->toArray();
|
||||
}
|
||||
|
||||
return array_map([$this, 'normalizeParameterData'], $parameters);
|
||||
}
|
||||
|
||||
protected function normalizeParameterData(array $data): array
|
||||
{
|
||||
$data['type'] = static::normalizeTypeName($data['type']);
|
||||
if (is_null($data['example'])) {
|
||||
$data['example'] = $this->generateDummyValue($data['type'], [
|
||||
'name' => $data['name'],
|
||||
'enumValues' => $data['enumValues'],
|
||||
]);
|
||||
} elseif ($data['example'] === 'No-example' || $data['example'] === 'No-example.') {
|
||||
$data['example'] = null;
|
||||
}
|
||||
|
||||
if ($data['required']) {
|
||||
$data['nullable'] = false;
|
||||
}
|
||||
|
||||
$data['description'] = mb_trim($data['description'] ?? '');
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
31
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Headers/GetFromHeaderAttribute.php
vendored
Normal file
31
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Headers/GetFromHeaderAttribute.php
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\Headers;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Attributes\Header;
|
||||
use Knuckles\Scribe\Extracting\Strategies\PhpAttributeStrategy;
|
||||
|
||||
/**
|
||||
* @extends PhpAttributeStrategy<Header>
|
||||
*/
|
||||
class GetFromHeaderAttribute extends PhpAttributeStrategy
|
||||
{
|
||||
protected static array $attributeNames = [Header::class];
|
||||
|
||||
protected function extractFromAttributes(
|
||||
ExtractedEndpointData $endpointData,
|
||||
array $attributesOnMethod,
|
||||
array $attributesOnFormRequest = [],
|
||||
array $attributesOnController = [],
|
||||
): ?array {
|
||||
$headers = [];
|
||||
foreach ([...$attributesOnController, ...$attributesOnFormRequest, ...$attributesOnMethod] as $attributeInstance) {
|
||||
$data = $attributeInstance->toArray();
|
||||
$data['example'] ??= $this->generateDummyValue('string');
|
||||
$headers[$data['name']] = $data['example'];
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
39
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Headers/GetFromHeaderTag.php
vendored
Normal file
39
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Headers/GetFromHeaderTag.php
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\Headers;
|
||||
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
use Knuckles\Scribe\Extracting\Strategies\TagStrategyWithFormRequestFallback;
|
||||
use Knuckles\Scribe\Tools\Utils;
|
||||
use Mpociot\Reflection\DocBlock\Tag;
|
||||
|
||||
class GetFromHeaderTag extends TagStrategyWithFormRequestFallback
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
/**
|
||||
* @param Tag[] $tagsOnMethod
|
||||
* @param Tag[] $tagsOnClass
|
||||
*/
|
||||
public function getFromTags(array $tagsOnMethod, array $tagsOnClass = []): array
|
||||
{
|
||||
$headerTags = Utils::filterDocBlockTags([...$tagsOnClass, ...$tagsOnMethod], 'header');
|
||||
$headers = collect($headerTags)->mapWithKeys(function (Tag $tag) {
|
||||
// Format:
|
||||
// @header <name> <example>
|
||||
// Examples:
|
||||
// @header X-Custom An API header
|
||||
preg_match('/([\S]+)(.*)?/', $tag->getContent(), $content);
|
||||
|
||||
[$_, $name, $example] = $content;
|
||||
$example = mb_trim($example);
|
||||
if (empty($example)) {
|
||||
$example = $this->generateDummyValue('string');
|
||||
}
|
||||
|
||||
return [$name => $example];
|
||||
})->toArray();
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
156
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php
vendored
Normal file
156
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\Metadata;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\RouteDocBlocker;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
use Mpociot\Reflection\DocBlock;
|
||||
|
||||
class GetFromDocBlocks extends Strategy
|
||||
{
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): array
|
||||
{
|
||||
$docBlocks = RouteDocBlocker::getDocBlocksFromRoute($endpointData->route);
|
||||
$methodDocBlock = $docBlocks['method'];
|
||||
$classDocBlock = $docBlocks['class'];
|
||||
|
||||
return $this->getMetadataFromDocBlock($methodDocBlock, $classDocBlock);
|
||||
}
|
||||
|
||||
public function getMetadataFromDocBlock(DocBlock $methodDocBlock, DocBlock $classDocBlock): array
|
||||
{
|
||||
[$groupName, $groupDescription, $title] = $this->getEndpointGroupAndTitleDetails($methodDocBlock, $classDocBlock);
|
||||
|
||||
$metadata = [
|
||||
'groupName' => $groupName,
|
||||
'groupDescription' => $groupDescription,
|
||||
'subgroup' => $this->getEndpointSubGroup($methodDocBlock, $classDocBlock),
|
||||
'subgroupDescription' => $this->getEndpointSubGroupDescription($methodDocBlock, $classDocBlock),
|
||||
'title' => $title ?: $methodDocBlock->getShortDescription(),
|
||||
'description' => $methodDocBlock->getLongDescription()->getContents(),
|
||||
'deprecated' => $this->getDeprecatedStatusFromDocBlock($methodDocBlock, $classDocBlock),
|
||||
];
|
||||
if (! is_null($authStatus = $this->getAuthStatusFromDocBlock($methodDocBlock, $classDocBlock))) {
|
||||
$metadata['authenticated'] = $authStatus;
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
protected function getAuthStatusFromDocBlock(DocBlock $methodDocBlock, ?DocBlock $classDocBlock = null): ?bool
|
||||
{
|
||||
foreach ($methodDocBlock->getTags() as $tag) {
|
||||
if (mb_strtolower($tag->getName()) === 'authenticated') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mb_strtolower($tag->getName()) === 'unauthenticated') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $classDocBlock
|
||||
? $this->getAuthStatusFromDocBlock($classDocBlock)
|
||||
: null;
|
||||
}
|
||||
|
||||
protected function getDeprecatedStatusFromDocBlock(DocBlock $methodDocBlock, ?DocBlock $classDocBlock = null): bool|string
|
||||
{
|
||||
foreach ($methodDocBlock->getTags() as $tag) {
|
||||
if (mb_strtolower($tag->getName()) === 'deprecated') {
|
||||
return $tag->getContent() === '' ? true : $tag->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
if ($classDocBlock instanceof DocBlock) {
|
||||
return $this->getDeprecatedStatusFromDocBlock($classDocBlock);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array The endpoint's group name, the group description, and the endpoint title
|
||||
*/
|
||||
protected function getEndpointGroupAndTitleDetails(DocBlock $methodDocBlock, DocBlock $controllerDocBlock)
|
||||
{
|
||||
foreach ($methodDocBlock->getTags() as $tag) {
|
||||
if ($tag->getName() === 'group') {
|
||||
$endpointGroupParts = explode("\n", mb_trim($tag->getContent()));
|
||||
$endpointGroupName = array_shift($endpointGroupParts);
|
||||
$endpointGroupDescription = mb_trim(implode("\n", $endpointGroupParts));
|
||||
|
||||
// If the endpoint has no title (the methodDocBlock's "short description"),
|
||||
// we'll assume the endpointGroupDescription is actually the title
|
||||
// Something like this:
|
||||
// /**
|
||||
// * Fetch cars. <-- This is endpoint title.
|
||||
// * @group Cars <-- This is group name.
|
||||
// * APIs for cars. <-- This is group description (not required).
|
||||
// **/
|
||||
// VS
|
||||
// /**
|
||||
// * @group Cars <-- This is group name.
|
||||
// * Fetch cars. <-- This is endpoint title, NOT group description.
|
||||
// **/
|
||||
|
||||
// BTW, this is a spaghetti way of doing this.
|
||||
// It shall be refactored soon. Deus vult!💪
|
||||
// ...Or maybe not
|
||||
if (empty($methodDocBlock->getShortDescription())) {
|
||||
return [$endpointGroupName, '', $endpointGroupDescription];
|
||||
}
|
||||
|
||||
return [$endpointGroupName, $endpointGroupDescription, $methodDocBlock->getShortDescription()];
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the controller
|
||||
foreach ($controllerDocBlock->getTags() as $tag) {
|
||||
if ($tag->getName() === 'group') {
|
||||
$endpointGroupParts = explode("\n", mb_trim($tag->getContent()));
|
||||
$endpointGroupName = array_shift($endpointGroupParts);
|
||||
$endpointGroupDescription = implode("\n", $endpointGroupParts);
|
||||
|
||||
return [$endpointGroupName, $endpointGroupDescription, $methodDocBlock->getShortDescription()];
|
||||
}
|
||||
}
|
||||
|
||||
return [null, '', $methodDocBlock->getShortDescription()];
|
||||
}
|
||||
|
||||
protected function getEndpointSubGroup(DocBlock $methodDocBlock, DocBlock $controllerDocBlock): ?string
|
||||
{
|
||||
foreach ($methodDocBlock->getTags() as $tag) {
|
||||
if (mb_strtolower($tag->getName()) === 'subgroup') {
|
||||
return mb_trim($tag->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($controllerDocBlock->getTags() as $tag) {
|
||||
if (mb_strtolower($tag->getName()) === 'subgroup') {
|
||||
return mb_trim($tag->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getEndpointSubGroupDescription(DocBlock $methodDocBlock, DocBlock $controllerDocBlock): ?string
|
||||
{
|
||||
foreach ($methodDocBlock->getTags() as $tag) {
|
||||
if (mb_strtolower($tag->getName()) === 'subgroupdescription') {
|
||||
return mb_trim($tag->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($controllerDocBlock->getTags() as $tag) {
|
||||
if (mb_strtolower($tag->getName()) === 'subgroupdescription') {
|
||||
return mb_trim($tag->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
51
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Metadata/GetFromMetadataAttributes.php
vendored
Normal file
51
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Metadata/GetFromMetadataAttributes.php
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\Metadata;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Attributes\Authenticated;
|
||||
use Knuckles\Scribe\Attributes\Deprecated;
|
||||
use Knuckles\Scribe\Attributes\Endpoint;
|
||||
use Knuckles\Scribe\Attributes\Group;
|
||||
use Knuckles\Scribe\Attributes\Subgroup;
|
||||
use Knuckles\Scribe\Attributes\Unauthenticated;
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
use Knuckles\Scribe\Extracting\Strategies\PhpAttributeStrategy;
|
||||
|
||||
/**
|
||||
* @extends PhpAttributeStrategy<Authenticated|Endpoint|Group|Subgroup>
|
||||
*/
|
||||
class GetFromMetadataAttributes extends PhpAttributeStrategy
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
protected static array $attributeNames = [
|
||||
Group::class,
|
||||
Subgroup::class,
|
||||
Endpoint::class,
|
||||
Authenticated::class,
|
||||
Unauthenticated::class,
|
||||
Deprecated::class,
|
||||
];
|
||||
|
||||
protected function extractFromAttributes(
|
||||
ExtractedEndpointData $endpointData,
|
||||
array $attributesOnMethod,
|
||||
array $attributesOnFormRequest = [],
|
||||
array $attributesOnController = [],
|
||||
): ?array {
|
||||
$metadata = [
|
||||
'groupName' => '',
|
||||
'groupDescription' => '',
|
||||
'subgroup' => '',
|
||||
'subgroupDescription' => '',
|
||||
'title' => '',
|
||||
'description' => '',
|
||||
];
|
||||
foreach ([...$attributesOnController, ...$attributesOnFormRequest, ...$attributesOnMethod] as $attributeInstance) {
|
||||
$metadata = array_merge($metadata, $attributeInstance->toArray());
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
}
|
||||
66
vendor/knuckleswtf/scribe/src/Extracting/Strategies/PhpAttributeStrategy.php
vendored
Normal file
66
vendor/knuckleswtf/scribe/src/Extracting/Strategies/PhpAttributeStrategy.php
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\FindsFormRequestForMethod;
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
abstract class PhpAttributeStrategy extends Strategy
|
||||
{
|
||||
use FindsFormRequestForMethod;
|
||||
use ParamHelpers;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected static array $attributeNames;
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): array
|
||||
{
|
||||
$this->endpointData = $endpointData;
|
||||
[$attributesOnMethod, $attributesOnFormRequest, $attributesOnController]
|
||||
= $this->getAttributes($endpointData->method, $endpointData->controller);
|
||||
|
||||
return $this->extractFromAttributes($endpointData, $attributesOnMethod, $attributesOnFormRequest, $attributesOnController);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{array<T>, array<T>, array<T>}
|
||||
*/
|
||||
protected function getAttributes(\ReflectionFunctionAbstract $method, ?\ReflectionClass $class = null): array
|
||||
{
|
||||
$attributesOnMethod = collect(static::$attributeNames)
|
||||
->flatMap(fn (string $name) => $method->getAttributes($name, \ReflectionAttribute::IS_INSTANCEOF))
|
||||
->map(fn (\ReflectionAttribute $a) => $a->newInstance())->all();
|
||||
|
||||
// If there's a FormRequest, we check there.
|
||||
if ($formRequestClass = $this->getFormRequestReflectionClass($method)) {
|
||||
$attributesOnFormRequest = collect(static::$attributeNames)
|
||||
->flatMap(fn (string $name) => $formRequestClass->getAttributes($name, \ReflectionAttribute::IS_INSTANCEOF))
|
||||
->map(fn (\ReflectionAttribute $a) => $a->newInstance())->all();
|
||||
}
|
||||
|
||||
if ($class) {
|
||||
$attributesOnController = collect(static::$attributeNames)
|
||||
->flatMap(fn (string $name) => $class->getAttributes($name, \ReflectionAttribute::IS_INSTANCEOF))
|
||||
->map(fn (\ReflectionAttribute $a) => $a->newInstance())->all();
|
||||
}
|
||||
|
||||
return [$attributesOnMethod, $attributesOnFormRequest ?? [], $attributesOnController ?? []];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<T> $attributesOnMethod
|
||||
* @param array<T> $attributesOnController
|
||||
*/
|
||||
abstract protected function extractFromAttributes(
|
||||
ExtractedEndpointData $endpointData,
|
||||
array $attributesOnMethod,
|
||||
array $attributesOnFormRequest = [],
|
||||
array $attributesOnController = [],
|
||||
): ?array;
|
||||
}
|
||||
22
vendor/knuckleswtf/scribe/src/Extracting/Strategies/QueryParameters/GetFromFormRequest.php
vendored
Normal file
22
vendor/knuckleswtf/scribe/src/Extracting/Strategies/QueryParameters/GetFromFormRequest.php
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\QueryParameters;
|
||||
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetFromFormRequestBase;
|
||||
|
||||
class GetFromFormRequest extends GetFromFormRequestBase
|
||||
{
|
||||
protected string $customParameterDataMethodName = 'queryParameters';
|
||||
|
||||
protected function isFormRequestMeantForThisStrategy(\ReflectionClass $formRequestReflectionClass): bool
|
||||
{
|
||||
// Only use this FormRequest for query params if there's "Query parameters" in the docblock
|
||||
// Or there's a queryParameters() method
|
||||
$formRequestDocBlock = $formRequestReflectionClass->getDocComment();
|
||||
if (mb_strpos(mb_strtolower($formRequestDocBlock), 'query parameters') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::isFormRequestMeantForThisStrategy($formRequestReflectionClass);
|
||||
}
|
||||
}
|
||||
21
vendor/knuckleswtf/scribe/src/Extracting/Strategies/QueryParameters/GetFromInlineValidator.php
vendored
Normal file
21
vendor/knuckleswtf/scribe/src/Extracting/Strategies/QueryParameters/GetFromInlineValidator.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\QueryParameters;
|
||||
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetFromInlineValidatorBase;
|
||||
use PhpParser\Node;
|
||||
|
||||
class GetFromInlineValidator extends GetFromInlineValidatorBase
|
||||
{
|
||||
protected function isValidationStatementMeantForThisStrategy(Node $validationStatement): bool
|
||||
{
|
||||
// Only use this validator for query params if there's a "// Query parameters" comment above
|
||||
$comments = $validationStatement->getComments();
|
||||
$comments = implode("\n", array_map(fn ($comment) => $comment->getReformattedText(), $comments));
|
||||
if (mb_strpos(mb_strtolower($comments), 'query parameters') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\QueryParameters;
|
||||
|
||||
use Knuckles\Scribe\Attributes\QueryParam;
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetParamsFromAttributeStrategy;
|
||||
|
||||
/**
|
||||
* @extends GetParamsFromAttributeStrategy<QueryParam>
|
||||
*/
|
||||
class GetFromQueryParamAttribute extends GetParamsFromAttributeStrategy
|
||||
{
|
||||
protected static array $attributeNames = [QueryParam::class];
|
||||
}
|
||||
80
vendor/knuckleswtf/scribe/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php
vendored
Normal file
80
vendor/knuckleswtf/scribe/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\QueryParameters;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetFieldsFromTagStrategy;
|
||||
|
||||
class GetFromQueryParamTag extends GetFieldsFromTagStrategy
|
||||
{
|
||||
protected string $tagName = 'queryParam';
|
||||
|
||||
public function parseTag(string $tagContent): array
|
||||
{
|
||||
// Format:
|
||||
// @queryParam <name> <type (optional)> <"required" (optional)> <"deprecated" (optional)> <description>
|
||||
// Examples:
|
||||
// @queryParam text required The text.
|
||||
// @queryParam user_id integer The ID of the user.
|
||||
// @queryParam sort string deprecated Use `order` parameter instead.
|
||||
preg_match('/(.+?)\s+([a-zA-Z\[\]]+\s+)?(required\s+)?(deprecated\s+)?([\s\S]*)/', $tagContent, $content);
|
||||
|
||||
if (empty($content)) {
|
||||
// This means only name was supplied
|
||||
$name = $tagContent;
|
||||
$required = false;
|
||||
$deprecated = false;
|
||||
$description = '';
|
||||
$type = 'string';
|
||||
} else {
|
||||
[$_, $name, $type, $required, $deprecated, $description] = $content;
|
||||
|
||||
$description = mb_trim(str_replace(['No-example.', 'No-example'], '', $description));
|
||||
if ($description === 'required') {
|
||||
// No description was supplied
|
||||
$required = true;
|
||||
$description = '';
|
||||
} elseif ($description === 'deprecated') {
|
||||
$deprecated = true;
|
||||
$description = '';
|
||||
} else {
|
||||
$required = mb_trim($required) === 'required';
|
||||
$deprecated = mb_trim($deprecated) === 'deprecated';
|
||||
}
|
||||
|
||||
$type = mb_trim($type);
|
||||
if ($type) {
|
||||
if ($type === 'required') {
|
||||
// Type wasn't supplied
|
||||
$type = 'string';
|
||||
$required = true;
|
||||
} elseif ($type === 'deprecated') {
|
||||
// Type wasn't supplied but deprecated was
|
||||
$type = 'string';
|
||||
$deprecated = true;
|
||||
} else {
|
||||
$type = static::normalizeTypeName($type);
|
||||
// Type in annotation is optional
|
||||
if (! $this->isSupportedTypeInDocBlocks($type)) {
|
||||
// Then that wasn't a type, but part of the description
|
||||
$description = mb_trim("{$type} {$description}");
|
||||
$type = '';
|
||||
}
|
||||
}
|
||||
} elseif ($this->isSupportedTypeInDocBlocks($description)) {
|
||||
// Only type was supplied
|
||||
$type = $description;
|
||||
$description = '';
|
||||
}
|
||||
|
||||
$type = empty($type)
|
||||
? (Str::contains(mb_strtolower($description), ['number', 'count', 'page']) ? 'integer' : 'string')
|
||||
: static::normalizeTypeName($type);
|
||||
}
|
||||
|
||||
[$description, $example, $enumValues, $exampleWasSpecified]
|
||||
= $this->getDescriptionAndExample($description, $type, $tagContent, $name);
|
||||
|
||||
return compact('name', 'description', 'required', 'deprecated', 'example', 'type', 'enumValues', 'exampleWasSpecified');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\ResponseFields;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Attributes\ResponseField;
|
||||
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
|
||||
use Knuckles\Scribe\Extracting\Shared\ResponseFieldTools;
|
||||
use Knuckles\Scribe\Extracting\Strategies\PhpAttributeStrategy;
|
||||
use Knuckles\Scribe\Tools\Utils as u;
|
||||
|
||||
/**
|
||||
* @extends PhpAttributeStrategy<ResponseField>
|
||||
*/
|
||||
class GetFromResponseFieldAttribute extends PhpAttributeStrategy
|
||||
{
|
||||
protected static array $attributeNames = [ResponseField::class];
|
||||
|
||||
protected function extractFromAttributes(
|
||||
ExtractedEndpointData $endpointData,
|
||||
array $attributesOnMethod,
|
||||
array $attributesOnFormRequest = [],
|
||||
array $attributesOnController = [],
|
||||
): ?array {
|
||||
return [
|
||||
...$this->getNonApiResourceFields($endpointData, $attributesOnMethod, $attributesOnFormRequest, $attributesOnController),
|
||||
...$this->getApiResourceFields($endpointData),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getApiResourceFields(ExtractedEndpointData $endpointData): array
|
||||
{
|
||||
$apiResourceAttributes = $endpointData->method->getAttributes(ResponseFromApiResource::class);
|
||||
|
||||
return collect($apiResourceAttributes)
|
||||
->flatMap(fn (\ReflectionAttribute $attribute) => $this->extractFieldsFromApiResource($attribute, $endpointData))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
protected function extractFieldsFromApiResource(\ReflectionAttribute $attribute, ExtractedEndpointData $endpointData): array
|
||||
{
|
||||
$className = $attribute->newInstance()->name;
|
||||
$method = u::getReflectedRouteMethod([$className, 'toArray']);
|
||||
$wrapKey = $className::$wrap ?? null;
|
||||
|
||||
return collect($method->getAttributes(ResponseField::class))
|
||||
->mapWithKeys(function (\ReflectionAttribute $attr) use ($endpointData, $wrapKey) {
|
||||
$data = $attr->newInstance()->toArray();
|
||||
$data['type'] = ResponseFieldTools::inferTypeOfResponseField($data, $endpointData);
|
||||
|
||||
if ($wrapKey !== null) {
|
||||
$data['name'] = $wrapKey.'.'.$data['name'];
|
||||
}
|
||||
|
||||
return [$data['name'] => $data];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
protected function getNonApiResourceFields(
|
||||
ExtractedEndpointData $endpointData,
|
||||
array $attributesOnMethod,
|
||||
array $attributesOnFormRequest,
|
||||
array $attributesOnController,
|
||||
): array {
|
||||
return collect([...$attributesOnController, ...$attributesOnFormRequest, ...$attributesOnMethod])
|
||||
->mapWithKeys(function ($attributeInstance) use ($endpointData) {
|
||||
/** @var ResponseField $attributeInstance */
|
||||
$data = $attributeInstance->toArray();
|
||||
|
||||
$data['type'] = ResponseFieldTools::inferTypeOfResponseField($data, $endpointData);
|
||||
|
||||
return [$data['name'] => $data];
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
125
vendor/knuckleswtf/scribe/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php
vendored
Normal file
125
vendor/knuckleswtf/scribe/src/Extracting/Strategies/ResponseFields/GetFromResponseFieldTag.php
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\ResponseFields;
|
||||
|
||||
use Knuckles\Scribe\Extracting\Shared\ResponseFieldTools;
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetFieldsFromTagStrategy;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Responses\UseApiResourceTags;
|
||||
use Knuckles\Scribe\Tools\AnnotationParser as a;
|
||||
use Knuckles\Scribe\Tools\Utils as u;
|
||||
use Mpociot\Reflection\DocBlock;
|
||||
|
||||
class GetFromResponseFieldTag extends GetFieldsFromTagStrategy
|
||||
{
|
||||
protected string $tagName = 'responseField';
|
||||
|
||||
/**
|
||||
* Get responseField tags from the controller method or the API resource class.
|
||||
*/
|
||||
public function getFromTags(array $tagsOnMethod, array $tagsOnClass = []): array
|
||||
{
|
||||
$nonApiResourceFields = parent::getFromTags($tagsOnMethod, $tagsOnClass);
|
||||
$apiResourceFields = $this->getApiResourceFields($tagsOnMethod);
|
||||
|
||||
return [...$nonApiResourceFields, ...$apiResourceFields];
|
||||
}
|
||||
|
||||
public function getClassNameFromApiResourceTag(string $apiResourceTag): string
|
||||
{
|
||||
['content' => $className] = a::parseIntoContentAndFields($apiResourceTag, UseApiResourceTags::apiResourceAllowedFields());
|
||||
|
||||
return $className;
|
||||
}
|
||||
|
||||
protected function parseTag(string $tagContent): array
|
||||
{
|
||||
// Format:
|
||||
// @responseField <name> <type> <"required" (optional)> <description>
|
||||
// Examples:
|
||||
// @responseField text string required The text.
|
||||
// @responseField user_id integer The ID of the user.
|
||||
preg_match('/(.+?)\s+(.+?)\s+(.+?)\s+([\s\S]*)/', $tagContent, $content);
|
||||
if (empty($content)) {
|
||||
// This means only name and type were supplied
|
||||
[$name, $type] = preg_split('/\s+/', $tagContent);
|
||||
$description = '';
|
||||
$required = false;
|
||||
} else {
|
||||
[$_, $name, $type, $required, $description] = $content;
|
||||
if ($required !== 'required') {
|
||||
$description = $required.' '.$description;
|
||||
}
|
||||
|
||||
$required = $required === 'required';
|
||||
$description = mb_trim($description);
|
||||
}
|
||||
|
||||
$type = static::normalizeTypeName($type);
|
||||
$data = compact('name', 'type', 'required', 'description');
|
||||
|
||||
// Support optional type in annotation
|
||||
// The type can also be a union or nullable type (eg ?string or string|null)
|
||||
if (! $this->isSupportedTypeInDocBlocks(explode('|', mb_trim($type, '?'))[0])) {
|
||||
// Then that wasn't a type, but part of the description
|
||||
$data['description'] = mb_trim("{$type} {$description}");
|
||||
$data['type'] = '';
|
||||
|
||||
$data['type'] = ResponseFieldTools::inferTypeOfResponseField($data, $this->endpointData);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getApiResourceFields(array $tagsOnMethod): array
|
||||
{
|
||||
$apiResourceClassName = $this->getApiResourceClassName($tagsOnMethod);
|
||||
|
||||
if (empty($apiResourceClassName)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->extractFieldsFromApiResource($apiResourceClassName);
|
||||
}
|
||||
|
||||
protected function getApiResourceClassName(array $tagsOnMethod): ?string
|
||||
{
|
||||
$apiResourceTags = array_values(
|
||||
array_filter($tagsOnMethod, function ($tag) {
|
||||
return in_array(mb_strtolower($tag->getName()), ['apiresource', 'apiresourcecollection']);
|
||||
})
|
||||
);
|
||||
|
||||
if (empty($apiResourceTags)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getClassNameFromApiResourceTag($apiResourceTags[0]->getContent());
|
||||
}
|
||||
|
||||
protected function extractFieldsFromApiResource(string $className): array
|
||||
{
|
||||
$method = u::getReflectedRouteMethod([$className, 'toArray']);
|
||||
$docBlock = new DocBlock($method->getDocComment() ?: '');
|
||||
$tagsOnApiResource = $docBlock->getTags();
|
||||
|
||||
$wrapKey = $className::$wrap ?? null;
|
||||
$fields = parent::getFromTags($tagsOnApiResource, []);
|
||||
|
||||
return $this->applyWrapKeyPrefix($fields, $wrapKey);
|
||||
}
|
||||
|
||||
protected function applyWrapKeyPrefix(array $fields, ?string $wrapKey): array
|
||||
{
|
||||
if ($wrapKey === null) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$wrappedFields = [];
|
||||
foreach ($fields as $fieldName => $fieldData) {
|
||||
$fieldData['name'] = $wrapKey.'.'.$fieldData['name'];
|
||||
$wrappedFields[$fieldData['name']] = $fieldData;
|
||||
}
|
||||
|
||||
return $wrappedFields;
|
||||
}
|
||||
}
|
||||
356
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/ResponseCalls.php
vendored
Normal file
356
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/ResponseCalls.php
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\Responses;
|
||||
|
||||
use Illuminate\Contracts\Http\Kernel;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\DatabaseTransactionHelpers;
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\ErrorHandlingUtils as e;
|
||||
use Knuckles\Scribe\Tools\Globals;
|
||||
use Knuckles\Scribe\Tools\Utils;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* Make a call to the route and retrieve its response.
|
||||
*/
|
||||
class ResponseCalls extends Strategy
|
||||
{
|
||||
use DatabaseTransactionHelpers;
|
||||
use ParamHelpers;
|
||||
|
||||
protected array $previousConfigs = [];
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $settings = []): ?array
|
||||
{
|
||||
// Don't attempt a response call if there are already successful responses
|
||||
if ($endpointData->responses->hasSuccessResponse()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->makeResponseCall($endpointData, $settings);
|
||||
}
|
||||
|
||||
public function makeResponseCall(ExtractedEndpointData $endpointData, array $settings): ?array
|
||||
{
|
||||
$this->configureEnvironment($settings);
|
||||
|
||||
// Mix in parsed parameters with manually specified parameters.
|
||||
$bodyParameters = array_merge($endpointData->cleanBodyParameters, $settings['bodyParams'] ?? []);
|
||||
$queryParameters = array_merge($endpointData->cleanQueryParameters, $settings['queryParams'] ?? []);
|
||||
$urlParameters = $endpointData->cleanUrlParameters;
|
||||
$headers = $endpointData->headers;
|
||||
|
||||
if ($endpointData->auth) {
|
||||
[$where, $name, $value] = $endpointData->auth;
|
||||
|
||||
switch ($where) {
|
||||
case 'queryParameters':
|
||||
$queryParameters[$name] = $value;
|
||||
|
||||
break;
|
||||
|
||||
case 'bodyParameters':
|
||||
$bodyParameters[$name] = $value;
|
||||
|
||||
break;
|
||||
|
||||
case 'headers':
|
||||
$headers[$name] = $value;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException("Unknown auth location: {$where}");
|
||||
}
|
||||
}
|
||||
|
||||
$hardcodedFileParams = $settings['fileParams'] ?? [];
|
||||
$hardcodedFileParams = collect($hardcodedFileParams)->map(function ($filePath) {
|
||||
$fileName = basename($filePath);
|
||||
|
||||
return new UploadedFile(
|
||||
$filePath,
|
||||
$fileName,
|
||||
mime_content_type($filePath),
|
||||
test: true
|
||||
);
|
||||
})->toArray();
|
||||
$fileParameters = array_merge($endpointData->fileParameters, $hardcodedFileParams);
|
||||
|
||||
$request = $this->prepareRequest(
|
||||
$endpointData->route,
|
||||
$endpointData->uri,
|
||||
$settings,
|
||||
$urlParameters,
|
||||
$bodyParameters,
|
||||
$queryParameters,
|
||||
$fileParameters,
|
||||
$headers
|
||||
);
|
||||
|
||||
$this->runPreRequestHook($request, $endpointData);
|
||||
|
||||
try {
|
||||
$response = $this->makeApiCall($request, $endpointData->route);
|
||||
|
||||
$this->runPostRequestHook($request, $endpointData, $response);
|
||||
|
||||
$response = [
|
||||
[
|
||||
'status' => $response->getStatusCode(),
|
||||
'content' => $this->getContentFromResponse($response),
|
||||
'headers' => $this->getResponseHeaders($response),
|
||||
],
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
c::warn('Exception thrown during response call for'.$endpointData->name());
|
||||
e::dumpExceptionIfVerbose($e);
|
||||
|
||||
$response = null;
|
||||
} finally {
|
||||
$this->finish();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getMethods(Route $route): array
|
||||
{
|
||||
return array_diff($route->methods(), ['HEAD']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $only The routes which this strategy should be applied to. Can not be specified with $except.
|
||||
* Specify route names ("users.index", "users.*"), or method and path ("GET *", "POST /safe/*").
|
||||
* @param array $except The routes which this strategy should be applied to. Can not be specified with $only.
|
||||
* Specify route names ("users.index", "users.*"), or method and path ("GET *", "POST /safe/*").
|
||||
* @param array $config any extra Laravel config() values to before starting the response call
|
||||
* @param array $queryParams Query params to always send with the response call. Key-value array.
|
||||
* @param array $bodyParams Body params to always send with the response call. Key-value array.
|
||||
* @param array $fileParams File params to always send with the response call. Key-value array. Key is param name, value is file path.
|
||||
* @param array $cookies Cookies to always send with the response call. Key-value array.
|
||||
*/
|
||||
public static function withSettings(
|
||||
array $only = [],
|
||||
array $except = [],
|
||||
array $config = [],
|
||||
array $queryParams = [],
|
||||
array $bodyParams = [],
|
||||
array $fileParams = [
|
||||
// 'key' => 'storage/app/image.png',
|
||||
],
|
||||
array $cookies = [],
|
||||
): array {
|
||||
return static::wrapWithSettings(
|
||||
only: $only,
|
||||
except: $except,
|
||||
otherSettings: compact(
|
||||
'config',
|
||||
'queryParams',
|
||||
'bodyParams',
|
||||
'fileParams',
|
||||
'cookies',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function prepareRequest(
|
||||
Route $route,
|
||||
string $url,
|
||||
array $settings,
|
||||
array $urlParams,
|
||||
array $bodyParams,
|
||||
array $queryParams,
|
||||
array $fileParameters,
|
||||
array $headers,
|
||||
): Request {
|
||||
$uri = Utils::getUrlWithBoundParameters($url, $urlParams);
|
||||
$routeMethods = $this->getMethods($route);
|
||||
$method = array_shift($routeMethods);
|
||||
$cookies = $settings['cookies'] ?? [];
|
||||
|
||||
// Note that we initialise the request with the bodyParams here
|
||||
// and later still add them to the ParameterBag (`addBodyParameters`)
|
||||
// The first is so the body params get added to the request content
|
||||
// (where Laravel reads body from)
|
||||
// The second is so they get added to the request bag
|
||||
// (where Symfony usually reads from and Laravel sometimes does)
|
||||
// Adding to both ensures consistency
|
||||
|
||||
// Always use the current app domain for response calls
|
||||
$rootUrl = config('app.url');
|
||||
$request = Request::create(
|
||||
"{$rootUrl}/{$uri}",
|
||||
$method,
|
||||
[],
|
||||
$cookies,
|
||||
$fileParameters,
|
||||
$this->transformHeadersToServerVars($headers),
|
||||
json_encode($bodyParams)
|
||||
);
|
||||
// Add headers again to catch any ones we didn't transform properly.
|
||||
$this->addHeaders($request, $route, $headers);
|
||||
$this->addQueryParameters($request, $queryParams);
|
||||
$this->addBodyParameters($request, $bodyParams);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
protected function runPreRequestHook(Request $request, ExtractedEndpointData $endpointData): void
|
||||
{
|
||||
if (is_callable(Globals::$__beforeResponseCall)) {
|
||||
call_user_func_array(Globals::$__beforeResponseCall, [$request, $endpointData]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function runPostRequestHook(Request $request, ExtractedEndpointData $endpointData, mixed $response): void
|
||||
{
|
||||
if (is_callable(Globals::$__afterResponseCall)) {
|
||||
call_user_func_array(Globals::$__afterResponseCall, [$request, $endpointData, $response]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function makeApiCall(Request $request, Route $route)
|
||||
{
|
||||
return $this->callLaravelRoute($request);
|
||||
}
|
||||
|
||||
protected function callLaravelRoute(Request $request): Response
|
||||
{
|
||||
/** @var \Illuminate\Foundation\Http\Kernel $kernel */
|
||||
$kernel = app(Kernel::class);
|
||||
$response = $kernel->handle($request);
|
||||
$kernel->terminate($request, $response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform headers array to array of $_SERVER vars with HTTP_* format.
|
||||
*/
|
||||
protected function transformHeadersToServerVars(array $headers): array
|
||||
{
|
||||
$server = [];
|
||||
$prefix = 'HTTP_';
|
||||
foreach ($headers as $name => $value) {
|
||||
$name = strtr(mb_strtoupper($name), '-', '_');
|
||||
if (! Str::startsWith($name, $prefix) && $name !== 'CONTENT_TYPE') {
|
||||
$name = $prefix.$name;
|
||||
}
|
||||
$server[$name] = $value;
|
||||
}
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
protected function getResponseHeaders($response): array
|
||||
{
|
||||
$headers = $response->headers->all();
|
||||
$formattedHeaders = [];
|
||||
|
||||
foreach ($headers as $header => $values) {
|
||||
$formattedHeaders[$header] = implode('; ', $values);
|
||||
}
|
||||
|
||||
return $formattedHeaders;
|
||||
}
|
||||
|
||||
protected function getContentFromResponse(Response $response): false|string
|
||||
{
|
||||
if (! $response instanceof StreamedResponse) {
|
||||
return $response->getContent();
|
||||
}
|
||||
|
||||
// For streamed responses, the content is null, and only output directly via "echo" when we call "sendContent".
|
||||
// We use output buffering to capture the output into a new fake response.
|
||||
$renderedResponse = new Response('', $response->getStatusCode());
|
||||
$originalCallback = $response->getCallback();
|
||||
$response->setCallback(function () use ($originalCallback, $renderedResponse) {
|
||||
ob_start(function ($output) use ($renderedResponse) {
|
||||
$renderedResponse->setContent($output);
|
||||
});
|
||||
$originalCallback();
|
||||
ob_end_flush();
|
||||
});
|
||||
$response->sendContent();
|
||||
$renderedResponse->headers = $response->headers;
|
||||
|
||||
return $renderedResponse->getContent();
|
||||
}
|
||||
|
||||
private function configureEnvironment(array $settings)
|
||||
{
|
||||
$this->startDbTransaction();
|
||||
$this->setLaravelConfigs($settings['config'] ?? []);
|
||||
}
|
||||
|
||||
private function setLaravelConfigs(array $config)
|
||||
{
|
||||
if (empty($config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($config as $name => $value) {
|
||||
$this->previousConfigs[$name] = Config::get($name);
|
||||
Config::set([$name => $value]);
|
||||
}
|
||||
}
|
||||
|
||||
private function rollbackLaravelConfigChanges()
|
||||
{
|
||||
foreach ($this->previousConfigs as $name => $value) {
|
||||
Config::set([$name => $value]);
|
||||
}
|
||||
}
|
||||
|
||||
private function finish()
|
||||
{
|
||||
$this->endDbTransaction();
|
||||
$this->rollbackLaravelConfigChanges();
|
||||
}
|
||||
|
||||
private function addHeaders(Request $request, Route $route, ?array $headers): void
|
||||
{
|
||||
// Set the proper domain
|
||||
if ($route->getDomain()) {
|
||||
$request->headers->add([
|
||||
'HOST' => $route->getDomain(),
|
||||
]);
|
||||
$request->server->add([
|
||||
'HTTP_HOST' => $route->getDomain(),
|
||||
'SERVER_NAME' => $route->getDomain(),
|
||||
]);
|
||||
}
|
||||
|
||||
$headers = collect($headers);
|
||||
|
||||
if (($headers->get('Accept') ?: $headers->get('accept')) === 'application/json') {
|
||||
$request->setRequestFormat('json');
|
||||
}
|
||||
}
|
||||
|
||||
private function addQueryParameters(Request $request, array $query): void
|
||||
{
|
||||
$request->query->add($query);
|
||||
$request->server->add(['QUERY_STRING' => http_build_query($query)]);
|
||||
}
|
||||
|
||||
private function addBodyParameters(Request $request, array $body): void
|
||||
{
|
||||
$request->request->add($body);
|
||||
}
|
||||
}
|
||||
164
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseApiResourceTags.php
vendored
Normal file
164
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseApiResourceTags.php
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\Responses;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\DatabaseTransactionHelpers;
|
||||
use Knuckles\Scribe\Extracting\InstantiatesExampleModels;
|
||||
use Knuckles\Scribe\Extracting\RouteDocBlocker;
|
||||
use Knuckles\Scribe\Extracting\Shared\ApiResourceResponseTools;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
use Knuckles\Scribe\Tools\AnnotationParser as a;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\Utils;
|
||||
use Mpociot\Reflection\DocBlock;
|
||||
use Mpociot\Reflection\DocBlock\Tag;
|
||||
|
||||
/**
|
||||
* Parse an Eloquent API resource response from the docblock ( @apiResource || @apiResourcecollection ).
|
||||
*/
|
||||
class UseApiResourceTags extends Strategy
|
||||
{
|
||||
use DatabaseTransactionHelpers;
|
||||
use InstantiatesExampleModels;
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
|
||||
{
|
||||
$methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($endpointData->route)['method'];
|
||||
|
||||
$tags = $methodDocBlock->getTags();
|
||||
if (empty($apiResourceTag = $this->getApiResourceTag($tags))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getApiResourceResponseFromTags($apiResourceTag, $tags, $endpointData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a response from the @apiResource/@apiResourceCollection, @apiResourceModel and @apiResourceAdditional tags.
|
||||
*
|
||||
* @param Tag[] $allTags
|
||||
* @return null|array[]
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getApiResourceResponseFromTags(Tag $apiResourceTag, array $allTags, ExtractedEndpointData $endpointData): ?array
|
||||
{
|
||||
[$statusCode, $description, $apiResourceClass, $isCollection, $extra] = $this->getStatusCodeAndApiResourceClass($apiResourceTag);
|
||||
[$modelClass, $factoryStates, $relations, $pagination] = $this->getClassToBeTransformedAndAttributes($allTags, $apiResourceClass, $extra);
|
||||
$additionalData = $this->getAdditionalData($allTags);
|
||||
|
||||
$modelInstantiator = fn () => $this->instantiateExampleModel($modelClass, $factoryStates, $relations);
|
||||
|
||||
$this->startDbTransaction();
|
||||
$content = ApiResourceResponseTools::fetch(
|
||||
$apiResourceClass,
|
||||
$isCollection,
|
||||
$modelInstantiator,
|
||||
$endpointData,
|
||||
$pagination,
|
||||
$additionalData,
|
||||
);
|
||||
$this->endDbTransaction();
|
||||
|
||||
return [
|
||||
[
|
||||
'status' => $statusCode ?: 200,
|
||||
'description' => $description,
|
||||
'content' => $content,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// These fields were originally only set on @apiResourceModel, but now we also support them on @apiResource
|
||||
public static function apiResourceExtraFields()
|
||||
{
|
||||
return ['states', 'with', 'paginate'];
|
||||
}
|
||||
|
||||
public static function apiResourceAllowedFields()
|
||||
{
|
||||
return ['status', 'scenario', ...static::apiResourceExtraFields()];
|
||||
}
|
||||
|
||||
public static function apiResourceModelAllowedFields()
|
||||
{
|
||||
return ['states', 'with', 'paginate'];
|
||||
}
|
||||
|
||||
public function getApiResourceTag(array $tags): ?Tag
|
||||
{
|
||||
return Arr::first(Utils::filterDocBlockTags($tags, 'apiresource', 'apiresourcecollection'));
|
||||
}
|
||||
|
||||
protected function getClassToBeTransformedAndAttributes(array $tags, string $apiResourceClass, array $extra): array
|
||||
{
|
||||
$modelTag = Arr::first(Utils::filterDocBlockTags($tags, 'apiresourcemodel'));
|
||||
|
||||
$modelClass = null;
|
||||
|
||||
if ($modelTag) {
|
||||
['content' => $modelClass, 'fields' => $fields] = a::parseIntoContentAndFields($modelTag->getContent(), static::apiResourceModelAllowedFields());
|
||||
}
|
||||
|
||||
$fields = array_merge($extra, $fields ?? []);
|
||||
$states = $fields['states'] ? explode(',', $fields['states']) : [];
|
||||
$relations = $fields['with'] ? explode(',', $fields['with']) : [];
|
||||
$pagination = $fields['paginate'] ? explode(',', $fields['paginate']) : [];
|
||||
|
||||
if (empty($modelClass)) {
|
||||
$modelClass = ApiResourceResponseTools::tryToInferApiResourceModel($apiResourceClass);
|
||||
}
|
||||
|
||||
if (empty($modelClass)) {
|
||||
c::warn(
|
||||
<<<'WARN'
|
||||
Couldn't detect an Eloquent API resource model from your `@apiResource`.
|
||||
Either specify a model using the `@apiResourceModel` annotation, or add an `@mixin` annotation in your resource's docblock.
|
||||
WARN
|
||||
);
|
||||
}
|
||||
|
||||
return [$modelClass, $states, $relations, $pagination];
|
||||
}
|
||||
|
||||
private function getStatusCodeAndApiResourceClass(Tag $tag): array
|
||||
{
|
||||
preg_match('/^(\d{3})?\s?([\s\S]*)$/', $tag->getContent(), $result);
|
||||
|
||||
$status = $result[1] ?: 0;
|
||||
$content = $result[2];
|
||||
|
||||
[
|
||||
'fields' => $fields,
|
||||
'content' => $content,
|
||||
] = a::parseIntoContentAndFields($content, static::apiResourceAllowedFields());
|
||||
|
||||
$status = $fields['status'] ?: $status;
|
||||
$apiResourceClass = $content;
|
||||
$description = $fields['scenario'] ?: '';
|
||||
|
||||
$isCollection = mb_strtolower($tag->getName()) === 'apiresourcecollection';
|
||||
|
||||
return [
|
||||
(int) $status,
|
||||
$description,
|
||||
$apiResourceClass,
|
||||
$isCollection,
|
||||
collect($fields)->only(...static::apiResourceExtraFields())->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data for simulating JsonResource ->additional() function.
|
||||
*
|
||||
* @param Tag[] $tags
|
||||
*/
|
||||
private function getAdditionalData(array $tags): array
|
||||
{
|
||||
$tag = Arr::first(Utils::filterDocBlockTags($tags, 'apiresourceadditional'));
|
||||
|
||||
return $tag ? a::parseIntoFields($tag->getContent()) : [];
|
||||
}
|
||||
}
|
||||
125
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseResponseAttributes.php
vendored
Normal file
125
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseResponseAttributes.php
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\Responses;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Attributes\Response;
|
||||
use Knuckles\Scribe\Attributes\ResponseFromApiResource;
|
||||
use Knuckles\Scribe\Attributes\ResponseFromFile;
|
||||
use Knuckles\Scribe\Attributes\ResponseFromTransformer;
|
||||
use Knuckles\Scribe\Extracting\DatabaseTransactionHelpers;
|
||||
use Knuckles\Scribe\Extracting\InstantiatesExampleModels;
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
use Knuckles\Scribe\Extracting\Shared\ApiResourceResponseTools;
|
||||
use Knuckles\Scribe\Extracting\Shared\TransformerResponseTools;
|
||||
use Knuckles\Scribe\Extracting\Strategies\PhpAttributeStrategy;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
|
||||
/**
|
||||
* @extends PhpAttributeStrategy<Response|ResponseFromApiResource|ResponseFromFile|ResponseFromTransformer>
|
||||
*/
|
||||
class UseResponseAttributes extends PhpAttributeStrategy
|
||||
{
|
||||
use DatabaseTransactionHelpers;
|
||||
use InstantiatesExampleModels;
|
||||
use ParamHelpers;
|
||||
|
||||
protected static array $attributeNames = [
|
||||
Response::class,
|
||||
ResponseFromFile::class,
|
||||
ResponseFromApiResource::class,
|
||||
ResponseFromTransformer::class,
|
||||
];
|
||||
|
||||
protected function extractFromAttributes(
|
||||
ExtractedEndpointData $endpointData,
|
||||
array $attributesOnMethod,
|
||||
array $attributesOnFormRequest = [],
|
||||
array $attributesOnController = [],
|
||||
): ?array {
|
||||
$responses = [];
|
||||
foreach ([...$attributesOnController, ...$attributesOnFormRequest, ...$attributesOnMethod] as $attributeInstance) {
|
||||
// @phpstan-ignore-next-line
|
||||
$responses[] = match (true) {
|
||||
$attributeInstance instanceof Response => $attributeInstance->toArray(),
|
||||
$attributeInstance instanceof ResponseFromFile => $attributeInstance->toArray(),
|
||||
$attributeInstance instanceof ResponseFromApiResource => $this->getApiResourceResponse($attributeInstance),
|
||||
$attributeInstance instanceof ResponseFromTransformer => $this->getTransformerResponse($attributeInstance),
|
||||
};
|
||||
}
|
||||
|
||||
return $responses;
|
||||
}
|
||||
|
||||
protected function getApiResourceResponse(ResponseFromApiResource $attributeInstance)
|
||||
{
|
||||
$modelToBeTransformed = $attributeInstance->modelToBeTransformed();
|
||||
if (empty($modelToBeTransformed)) {
|
||||
c::warn(
|
||||
<<<'WARN'
|
||||
Couldn't detect an Eloquent API resource model from your ResponseFromApiResource.
|
||||
Either specify a model using the `model:` parameter, or add an `@mixin` annotation in your resource's docblock.
|
||||
WARN
|
||||
);
|
||||
$modelInstantiator = null;
|
||||
} else {
|
||||
$modelInstantiator = fn () => $this->instantiateExampleModel($modelToBeTransformed, $attributeInstance->factoryStates, $attributeInstance->with, null, $attributeInstance->withCount);
|
||||
}
|
||||
|
||||
$pagination = [];
|
||||
if ($attributeInstance->paginate) {
|
||||
$pagination = [$attributeInstance->paginate];
|
||||
} elseif ($attributeInstance->simplePaginate) {
|
||||
$pagination = [$attributeInstance->simplePaginate, 'simple'];
|
||||
} elseif ($attributeInstance->cursorPaginate) {
|
||||
$pagination = [$attributeInstance->cursorPaginate, 'cursor'];
|
||||
}
|
||||
|
||||
$this->startDbTransaction();
|
||||
$content = ApiResourceResponseTools::fetch(
|
||||
$attributeInstance->name,
|
||||
$attributeInstance->isCollection(),
|
||||
$modelInstantiator,
|
||||
$this->endpointData,
|
||||
$pagination,
|
||||
$attributeInstance->additional,
|
||||
);
|
||||
$this->endDbTransaction();
|
||||
|
||||
return [
|
||||
'status' => $attributeInstance->status,
|
||||
'description' => $attributeInstance->description,
|
||||
'content' => $content,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTransformerResponse(ResponseFromTransformer $attributeInstance)
|
||||
{
|
||||
$modelInstantiator = fn () => $this->instantiateExampleModel(
|
||||
$attributeInstance->model,
|
||||
$attributeInstance->factoryStates,
|
||||
$attributeInstance->with,
|
||||
(new \ReflectionClass($attributeInstance->name))->getMethod('transform')
|
||||
);
|
||||
|
||||
$pagination = $attributeInstance->paginate ? [
|
||||
'perPage' => $attributeInstance->paginate[1] ?? null, 'adapter' => $attributeInstance->paginate[0],
|
||||
] : [];
|
||||
$this->startDbTransaction();
|
||||
$content = TransformerResponseTools::fetch(
|
||||
$attributeInstance->name,
|
||||
$attributeInstance->collection,
|
||||
$modelInstantiator,
|
||||
$pagination,
|
||||
$attributeInstance->resourceKey,
|
||||
$this->config->get('fractal.serializer'),
|
||||
);
|
||||
$this->endDbTransaction();
|
||||
|
||||
return [
|
||||
'status' => $attributeInstance->status,
|
||||
'description' => $attributeInstance->description,
|
||||
'content' => $content,
|
||||
];
|
||||
}
|
||||
}
|
||||
54
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseResponseFileTag.php
vendored
Normal file
54
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseResponseFileTag.php
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\Responses;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\RouteDocBlocker;
|
||||
use Knuckles\Scribe\Extracting\Shared\ResponseFileTools;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
use Knuckles\Scribe\Tools\AnnotationParser as a;
|
||||
use Knuckles\Scribe\Tools\Utils;
|
||||
use Mpociot\Reflection\DocBlock\Tag;
|
||||
|
||||
/**
|
||||
* Get a response from a file path in the docblock ( @responseFile ).
|
||||
*/
|
||||
class UseResponseFileTag extends Strategy
|
||||
{
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
|
||||
{
|
||||
$docBlocks = RouteDocBlocker::getDocBlocksFromRoute($endpointData->route);
|
||||
|
||||
return $this->getFileResponses($docBlocks['method']->getTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag[] $tags
|
||||
*/
|
||||
public function getFileResponses(array $tags): ?array
|
||||
{
|
||||
$responseFileTags = Utils::filterDocBlockTags($tags, 'responsefile');
|
||||
|
||||
if (empty($responseFileTags)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_map(function (Tag $responseFileTag) {
|
||||
preg_match('/^(\d{3})?\s*(.*?)({.*})?$/', $responseFileTag->getContent(), $result);
|
||||
[$_, $status, $mainContent] = $result;
|
||||
$json = $result[3] ?? null;
|
||||
|
||||
['fields' => $fields, 'content' => $filePath] = a::parseIntoContentAndFields($mainContent, ['status', 'scenario']);
|
||||
|
||||
$status = $fields['status'] ?: ($status ?: 200);
|
||||
$description = $fields['scenario'] ?: '';
|
||||
$content = ResponseFileTools::getResponseContents($filePath, $json);
|
||||
|
||||
return [
|
||||
'content' => $content,
|
||||
'status' => (int) $status,
|
||||
'description' => $description,
|
||||
];
|
||||
}, $responseFileTags);
|
||||
}
|
||||
}
|
||||
50
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseResponseTag.php
vendored
Normal file
50
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseResponseTag.php
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\Responses;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\RouteDocBlocker;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
use Knuckles\Scribe\Tools\AnnotationParser as a;
|
||||
use Knuckles\Scribe\Tools\Utils;
|
||||
use Mpociot\Reflection\DocBlock\Tag;
|
||||
|
||||
/**
|
||||
* Get a response from the docblock ( @response ).
|
||||
*/
|
||||
class UseResponseTag extends Strategy
|
||||
{
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
|
||||
{
|
||||
$docBlocks = RouteDocBlocker::getDocBlocksFromRoute($endpointData->route);
|
||||
|
||||
return $this->getDocBlockResponses($docBlocks['method']->getTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag[] $tags
|
||||
*/
|
||||
public function getDocBlockResponses(array $tags): ?array
|
||||
{
|
||||
$responseTags = Utils::filterDocBlockTags($tags, 'response');
|
||||
|
||||
if (empty($responseTags)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_map(function (Tag $responseTag) {
|
||||
// Status code (optional) followed by response
|
||||
preg_match('/^(\d{3})?\s?([\s\S]*)$/', $responseTag->getContent(), $result);
|
||||
|
||||
$status = $result[1] ?: 200;
|
||||
$content = $result[2] ?: '{}';
|
||||
|
||||
['fields' => $fields, 'content' => $content] = a::parseIntoContentAndFields($content, ['status', 'scenario']);
|
||||
|
||||
$status = $fields['status'] ?: $status;
|
||||
$description = $fields['scenario'] ?: '';
|
||||
|
||||
return ['content' => $content, 'status' => (int) $status, 'description' => $description];
|
||||
}, $responseTags);
|
||||
}
|
||||
}
|
||||
132
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseTransformerTags.php
vendored
Normal file
132
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Responses/UseTransformerTags.php
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\Responses;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\DatabaseTransactionHelpers;
|
||||
use Knuckles\Scribe\Extracting\InstantiatesExampleModels;
|
||||
use Knuckles\Scribe\Extracting\RouteDocBlocker;
|
||||
use Knuckles\Scribe\Extracting\Shared\TransformerResponseTools;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
use Knuckles\Scribe\Tools\AnnotationParser as a;
|
||||
use Knuckles\Scribe\Tools\Utils;
|
||||
use Mpociot\Reflection\DocBlock\Tag;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($endpointData->route)['method'];
|
||||
$tags = $methodDocBlock->getTags();
|
||||
|
||||
return $this->getTransformerResponseFromTags($tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a response from the @transformer/@transformerCollection and @transformerModel tags.
|
||||
*
|
||||
* @param Tag[] $allTags
|
||||
*/
|
||||
public function getTransformerResponseFromTag(Tag $transformerTag, array $allTags): ?array
|
||||
{
|
||||
[$statusCode, $transformerClass, $isCollection] = $this->getStatusCodeAndTransformerClass($transformerTag);
|
||||
[$model, $factoryStates, $relations, $resourceKey] = $this->getClassToBeTransformed($allTags);
|
||||
|
||||
$modelInstantiator = fn () => $this->instantiateExampleModel($model, $factoryStates, $relations, (new \ReflectionClass($transformerClass))->getMethod('transform'));
|
||||
$pagination = $this->getTransformerPaginatorData($allTags);
|
||||
$serializer = $this->config->get('fractal.serializer');
|
||||
|
||||
$this->startDbTransaction();
|
||||
$content = TransformerResponseTools::fetch(
|
||||
$transformerClass,
|
||||
$isCollection,
|
||||
$modelInstantiator,
|
||||
$pagination,
|
||||
$resourceKey,
|
||||
$serializer
|
||||
);
|
||||
$this->endDbTransaction();
|
||||
|
||||
return [
|
||||
[
|
||||
'status' => $statusCode ?: 200,
|
||||
'content' => $content,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTransformerResponseFromTags(array $tags): ?array
|
||||
{
|
||||
if (empty($transformerTag = $this->getTransformerTag($tags))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getTransformerResponseFromTag($transformerTag, $tags);
|
||||
}
|
||||
|
||||
private function getStatusCodeAndTransformerClass(Tag $tag): array
|
||||
{
|
||||
preg_match('/^(\d{3})?\s?([\s\S]*)$/', $tag->getContent(), $result);
|
||||
$status = (int) ($result[1] ?: 200);
|
||||
$transformerClass = $result[2];
|
||||
$isCollection = mb_strtolower($tag->getName()) === 'transformercollection';
|
||||
|
||||
return [$status, $transformerClass, $isCollection];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getClassToBeTransformed(array $tags): array
|
||||
{
|
||||
$modelTag = Arr::first(Utils::filterDocBlockTags($tags, 'transformermodel'));
|
||||
|
||||
$type = null;
|
||||
$states = [];
|
||||
$relations = [];
|
||||
$resourceKey = null;
|
||||
if ($modelTag) {
|
||||
['content' => $type, 'fields' => $fields] = a::parseIntoContentAndFields($modelTag->getContent(), ['states', 'with', 'resourceKey']);
|
||||
$states = $fields['states'] ? explode(',', $fields['states']) : [];
|
||||
$relations = $fields['with'] ? explode(',', $fields['with']) : [];
|
||||
$resourceKey = $fields['resourceKey'] ?? null;
|
||||
}
|
||||
|
||||
return [$type, $states, $relations, $resourceKey];
|
||||
}
|
||||
|
||||
private function getTransformerTag(array $tags): ?Tag
|
||||
{
|
||||
return Arr::first(Utils::filterDocBlockTags($tags, 'transformer', 'transformercollection'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets pagination data from the `@transformerPaginator` tag, like this:
|
||||
* `@transformerPaginator League\Fractal\Pagination\IlluminatePaginatorAdapter 15`
|
||||
*
|
||||
* @param Tag[] $tags
|
||||
*/
|
||||
private function getTransformerPaginatorData(array $tags): array
|
||||
{
|
||||
$tag = Arr::first(Utils::filterDocBlockTags($tags, 'transformerpaginator'));
|
||||
if (empty($tag)) {
|
||||
return ['adapter' => null, 'perPage' => null];
|
||||
}
|
||||
|
||||
preg_match('/^\s*(.+?)(\s+\d+)?$/', $tag->getContent(), $result);
|
||||
$paginatorAdapter = $result[1];
|
||||
$perPage = $result[2] ?? null;
|
||||
if ($perPage) {
|
||||
$perPage = mb_trim($perPage);
|
||||
}
|
||||
|
||||
return ['adapter' => $paginatorAdapter, 'perPage' => $perPage ?: null];
|
||||
}
|
||||
}
|
||||
30
vendor/knuckleswtf/scribe/src/Extracting/Strategies/StaticData.php
vendored
Normal file
30
vendor/knuckleswtf/scribe/src/Extracting/Strategies/StaticData.php
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
|
||||
/**
|
||||
* A simple strategy that returns a set of static data.
|
||||
*/
|
||||
class StaticData extends Strategy
|
||||
{
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $settings = []): ?array
|
||||
{
|
||||
return $settings['data'];
|
||||
}
|
||||
|
||||
public static function withSettings(
|
||||
array $only = [],
|
||||
array $except = [],
|
||||
array $data = [],
|
||||
): array {
|
||||
return static::wrapWithSettings(
|
||||
only: $only,
|
||||
except: $except,
|
||||
otherSettings: compact(
|
||||
'data',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
47
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Strategy.php
vendored
Normal file
47
vendor/knuckleswtf/scribe/src/Extracting/Strategies/Strategy.php
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Tools\DocumentationConfig;
|
||||
|
||||
abstract class Strategy
|
||||
{
|
||||
public ?ExtractedEndpointData $endpointData;
|
||||
|
||||
public function __construct(protected DocumentationConfig $config) {}
|
||||
|
||||
/**
|
||||
* @param array $settings settings to be applied to this strategy
|
||||
*/
|
||||
abstract public function __invoke(ExtractedEndpointData $endpointData, array $settings = []): ?array;
|
||||
|
||||
/**
|
||||
* Returns an instance of the documentation config.
|
||||
*/
|
||||
public function getConfig(): DocumentationConfig
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that returns a tuple of [$strategyName, $settingsArray].
|
||||
* Main real advantage is that it validates the mutual exclusion of $only and $except.
|
||||
*
|
||||
* @param array $only The routes which this strategy should be applied to. Can not be specified with $except.
|
||||
* Specify route names ("users.index", "users.*"), or method and path ("GET *", "POST /safe/*").
|
||||
* @param array $except The routes which this strategy should be applied to. Can not be specified with $only.
|
||||
* Specify route names ("users.index", "users.*"), or method and path ("GET *", "POST /safe/*").
|
||||
* @return array{string,array} tuple of strategy class FQN and specified settings
|
||||
*/
|
||||
public static function wrapWithSettings(
|
||||
array $only = [],
|
||||
array $except = [],
|
||||
array $otherSettings = [],
|
||||
): array {
|
||||
return [
|
||||
static::class,
|
||||
['only' => $only, 'except' => $except, ...$otherSettings],
|
||||
];
|
||||
}
|
||||
}
|
||||
46
vendor/knuckleswtf/scribe/src/Extracting/Strategies/TagStrategyWithFormRequestFallback.php
vendored
Normal file
46
vendor/knuckleswtf/scribe/src/Extracting/Strategies/TagStrategyWithFormRequestFallback.php
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies;
|
||||
|
||||
use Illuminate\Routing\Route;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\FindsFormRequestForMethod;
|
||||
use Knuckles\Scribe\Extracting\RouteDocBlocker;
|
||||
use Mpociot\Reflection\DocBlock;
|
||||
use Mpociot\Reflection\DocBlock\Tag;
|
||||
|
||||
abstract class TagStrategyWithFormRequestFallback extends Strategy
|
||||
{
|
||||
use FindsFormRequestForMethod;
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
|
||||
{
|
||||
$this->endpointData = $endpointData;
|
||||
|
||||
return $this->getParametersFromDocBlockInFormRequestOrMethod($endpointData->route, $endpointData->method);
|
||||
}
|
||||
|
||||
public function getParametersFromDocBlockInFormRequestOrMethod(Route $route, \ReflectionFunctionAbstract $method): array
|
||||
{
|
||||
$classTags = RouteDocBlocker::getDocBlocksFromRoute($route)['class']?->getTags() ?: [];
|
||||
// If there's a FormRequest, w.e check there for tags.
|
||||
if ($formRequestClass = $this->getFormRequestReflectionClass($method)) {
|
||||
$formRequestDocBlock = new DocBlock($formRequestClass->getDocComment());
|
||||
$parametersFromFormRequest = $this->getFromTags($formRequestDocBlock->getTags(), $classTags);
|
||||
|
||||
if (count($parametersFromFormRequest)) {
|
||||
return $parametersFromFormRequest;
|
||||
}
|
||||
}
|
||||
|
||||
$methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method'];
|
||||
|
||||
return $this->getFromTags($methodDocBlock->getTags(), $classTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag[] $tagsOnMethod
|
||||
* @param Tag[] $tagsOnClass
|
||||
*/
|
||||
abstract public function getFromTags(array $tagsOnMethod, array $tagsOnClass = []): array;
|
||||
}
|
||||
225
vendor/knuckleswtf/scribe/src/Extracting/Strategies/UrlParameters/GetFromLaravelAPI.php
vendored
Normal file
225
vendor/knuckleswtf/scribe/src/Extracting/Strategies/UrlParameters/GetFromLaravelAPI.php
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\UrlParameters;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Extracting\ParamHelpers;
|
||||
use Knuckles\Scribe\Extracting\Shared\UrlParamsNormalizer;
|
||||
use Knuckles\Scribe\Extracting\Strategies\Strategy;
|
||||
|
||||
class GetFromLaravelAPI extends Strategy
|
||||
{
|
||||
use ParamHelpers;
|
||||
|
||||
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
|
||||
{
|
||||
$parameters = [];
|
||||
|
||||
$path = $endpointData->uri;
|
||||
preg_match_all('/\{(.*?)\}/', $path, $matches);
|
||||
|
||||
foreach ($matches[1] as $match) {
|
||||
$isOptional = Str::endsWith($match, '?');
|
||||
$name = mb_rtrim($match, '?');
|
||||
|
||||
$parameters[$name] = [
|
||||
'name' => $name,
|
||||
'description' => $this->inferUrlParamDescription($endpointData->uri, $name),
|
||||
'required' => ! $isOptional,
|
||||
];
|
||||
}
|
||||
|
||||
$parameters = $this->inferBetterTypesAndExamplesForEloquentUrlParameters($parameters, $endpointData);
|
||||
$parameters = $this->inferBetterTypesAndExamplesForEnumUrlParameters($parameters, $endpointData);
|
||||
|
||||
return $this->setTypesAndExamplesForOthers($parameters, $endpointData);
|
||||
}
|
||||
|
||||
protected function inferUrlParamDescription(string $url, string $paramName): string
|
||||
{
|
||||
// If $url is sth like /users/{id}, return "The ID of the user."
|
||||
// If $url is sth like /anything/{user_id}, return "The ID of the user."
|
||||
|
||||
$strategies = collect(['id', 'slug'])->map(function ($name) {
|
||||
$friendlyName = $name === 'id' ? 'ID' : $name;
|
||||
|
||||
return function ($url, $paramName) use ($name, $friendlyName) {
|
||||
if ($paramName === $name) {
|
||||
$thing = $this->getNameOfUrlThing($url, $paramName);
|
||||
|
||||
return "The {$friendlyName} of the {$thing}.";
|
||||
}
|
||||
if (Str::is("*_{$name}", $paramName)) {
|
||||
$thing = str_replace(['_', '-'], ' ', str_replace("_{$name}", '', $paramName));
|
||||
|
||||
return "The {$friendlyName} of the {$thing}.";
|
||||
}
|
||||
};
|
||||
})->toArray();
|
||||
|
||||
// If $url is sth like /categories/{category}, return "The category."
|
||||
$strategies[] = function ($url, $paramName) {
|
||||
$thing = $this->getNameOfUrlThing($url, $paramName);
|
||||
if ($thing === $paramName) {
|
||||
return "The {$thing}.";
|
||||
}
|
||||
};
|
||||
|
||||
foreach ($strategies as $strategy) {
|
||||
if ($inferred = $strategy($url, $paramName)) {
|
||||
return $inferred;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function inferBetterTypesAndExamplesForEloquentUrlParameters(array $parameters, ExtractedEndpointData $endpointData): array
|
||||
{
|
||||
// We'll gather Eloquent model instances that can be linked to a URl parameter
|
||||
$modelInstances = [];
|
||||
|
||||
// First, any bound models
|
||||
// Eg if route is /users/{id}, and (User $user) model is typehinted on method
|
||||
// If User model has `id` as an integer, then {id} param should be an integer
|
||||
$typeHintedEloquentModels = UrlParamsNormalizer::getTypeHintedEloquentModels($endpointData->method);
|
||||
foreach ($typeHintedEloquentModels as $argumentName => $modelInstance) {
|
||||
$routeKey = $modelInstance->getRouteKeyName();
|
||||
|
||||
// Find the param name. In our normalized URL, argument $user might be param {user}, or {user_id}, or {id},
|
||||
if (isset($parameters[$argumentName])) {
|
||||
$paramName = $argumentName;
|
||||
} elseif (isset($parameters["{$argumentName}_{$routeKey}"])) {
|
||||
$paramName = "{$argumentName}_{$routeKey}";
|
||||
} elseif (isset($parameters[$routeKey])) {
|
||||
$paramName = $routeKey;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$modelInstances[$paramName] = $modelInstance;
|
||||
}
|
||||
|
||||
// Next, non-Eloquent-bound parameters. They might still be Eloquent models, but model binding wasn't used.
|
||||
foreach ($parameters as $name => $data) {
|
||||
if (isset($data['type'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the url is /things/{id}, try to find a Thing model
|
||||
$urlThing = $this->getNameOfUrlThing($endpointData->uri, $name);
|
||||
if ($urlThing && ($modelInstance = $this->findModelFromUrlThing($urlThing))) {
|
||||
$modelInstances[$name] = $modelInstance;
|
||||
}
|
||||
}
|
||||
|
||||
// Now infer.
|
||||
foreach ($modelInstances as $paramName => $modelInstance) {
|
||||
// If the routeKey is the same as the primary key in the database, use the PK's type.
|
||||
$routeKey = $modelInstance->getRouteKeyName();
|
||||
$type = $modelInstance->getKeyName() === $routeKey
|
||||
? static::normalizeTypeName($modelInstance->getKeyType()) : 'string';
|
||||
|
||||
$parameters[$paramName]['type'] = $type;
|
||||
|
||||
try {
|
||||
$parameters[$paramName]['example'] = $modelInstance::first()->{$routeKey} ?? null;
|
||||
} catch (\Throwable) {
|
||||
$parameters[$paramName]['example'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
protected function inferBetterTypesAndExamplesForEnumUrlParameters(array $parameters, ExtractedEndpointData $endpointData): array
|
||||
{
|
||||
$typeHintedEnums = UrlParamsNormalizer::getTypeHintedEnums($endpointData->method);
|
||||
foreach ($typeHintedEnums as $argumentName => $enum) {
|
||||
$parameters[$argumentName]['type'] = static::normalizeTypeName($enum->getBackingType());
|
||||
|
||||
try {
|
||||
$parameters[$argumentName]['example'] = $enum->getCases()[0]->getBackingValue();
|
||||
} catch (\Throwable) {
|
||||
$parameters[$argumentName]['example'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
protected function setTypesAndExamplesForOthers(array $parameters, ExtractedEndpointData $endpointData): array
|
||||
{
|
||||
foreach ($parameters as $name => $parameter) {
|
||||
if (empty($parameter['type'])) {
|
||||
$parameters[$name]['type'] = 'string';
|
||||
}
|
||||
|
||||
if (($parameter['example'] ?? null) === null) {
|
||||
// If the user explicitly set a `where()` constraint, use that to refine examples
|
||||
$parameterRegex = $endpointData->route->wheres[$name] ?? null;
|
||||
$parameters[$name]['example'] = $parameterRegex
|
||||
? $this->castToType($this->getFaker()->regexify($parameterRegex), $parameters[$name]['type'])
|
||||
: $this->generateDummyValue($parameters[$name]['type'], hints: ['name' => $name]);
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL parameter $paramName, extract the "thing" that comes before it. eg::
|
||||
* - /<whatever>/things/{paramName} -> "thing"
|
||||
* - animals/cats/{id} -> "cat"
|
||||
* - users/{user_id}/contracts -> "user"
|
||||
*
|
||||
* @param null|string $alternateParamName a second paramName to try, if the original paramName isn't in the URL
|
||||
*/
|
||||
protected function getNameOfUrlThing(string $url, string $paramName, ?string $alternateParamName = null): ?string
|
||||
{
|
||||
$parts = explode('/', $url);
|
||||
if (count($parts) === 1) {
|
||||
return null;
|
||||
} // URL was "/{thing}"
|
||||
|
||||
$paramIndex = array_search("{{$paramName}}", $parts);
|
||||
|
||||
if ($paramIndex === false) {
|
||||
$paramIndex = array_search("{{$alternateParamName}}", $parts);
|
||||
}
|
||||
|
||||
if ($paramIndex === false || $paramIndex === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$things = $parts[$paramIndex - 1];
|
||||
|
||||
// Replace underscores/hyphens, so "side_projects" becomes "side project"
|
||||
return str_replace(['_', '-'], ' ', Str::singular($things));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL "thing", like the "cat" in /cats/{id}, try to locate a Cat model.
|
||||
*/
|
||||
protected function findModelFromUrlThing(string $urlThing): ?Model
|
||||
{
|
||||
$className = str_replace(['-', '_', ' '], '', Str::title($urlThing));
|
||||
$rootNamespace = app()->getNamespace();
|
||||
|
||||
if (class_exists($class = "{$rootNamespace}Models\\".$className, autoload: false)
|
||||
// For the heathens that don't use a Models\ directory
|
||||
|| class_exists($class = $rootNamespace.$className, autoload: false)) {
|
||||
try {
|
||||
$instance = new $class;
|
||||
} catch (\Error) { // It might be an enum or some other non-instantiable class
|
||||
return null;
|
||||
}
|
||||
|
||||
return $instance instanceof Model ? $instance : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
14
vendor/knuckleswtf/scribe/src/Extracting/Strategies/UrlParameters/GetFromUrlParamAttribute.php
vendored
Normal file
14
vendor/knuckleswtf/scribe/src/Extracting/Strategies/UrlParameters/GetFromUrlParamAttribute.php
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\UrlParameters;
|
||||
|
||||
use Knuckles\Scribe\Attributes\UrlParam;
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetParamsFromAttributeStrategy;
|
||||
|
||||
/**
|
||||
* @extends GetParamsFromAttributeStrategy<UrlParam>
|
||||
*/
|
||||
class GetFromUrlParamAttribute extends GetParamsFromAttributeStrategy
|
||||
{
|
||||
protected static array $attributeNames = [UrlParam::class];
|
||||
}
|
||||
54
vendor/knuckleswtf/scribe/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php
vendored
Normal file
54
vendor/knuckleswtf/scribe/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Extracting\Strategies\UrlParameters;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Scribe\Extracting\Strategies\GetFieldsFromTagStrategy;
|
||||
|
||||
class GetFromUrlParamTag extends GetFieldsFromTagStrategy
|
||||
{
|
||||
protected string $tagName = 'urlParam';
|
||||
|
||||
protected function parseTag(string $tagContent): array
|
||||
{
|
||||
// Format:
|
||||
// @urlParam <name> <type (optional)> <"required" (optional)> <description>
|
||||
// Examples:
|
||||
// @urlParam id string required The id of the post.
|
||||
// @urlParam user_id The ID of the user.
|
||||
|
||||
// We match on all the possible types for URL parameters. It's a limited range, so no biggie.
|
||||
preg_match('/(\w+?)\s+((int|integer|string|float|double|number)\s+)?(required\s+)?([\s\S]*)/', $tagContent, $content);
|
||||
if (empty($content)) {
|
||||
// This means only name was supplied
|
||||
$name = $tagContent;
|
||||
$required = false;
|
||||
$description = '';
|
||||
$type = 'string';
|
||||
} else {
|
||||
[$_, $name, $__, $type, $required, $description] = $content;
|
||||
$description = mb_trim(str_replace(['No-example.', 'No-example'], '', $description));
|
||||
if ($description === 'required') {
|
||||
$required = true;
|
||||
$description = '';
|
||||
} else {
|
||||
$required = mb_trim($required) === 'required';
|
||||
}
|
||||
|
||||
if (empty($type) && $this->isSupportedTypeInDocBlocks($description)) {
|
||||
// Only type was supplied
|
||||
$type = $description;
|
||||
$description = '';
|
||||
}
|
||||
|
||||
$type = empty($type)
|
||||
? (Str::contains($description, ['number', 'count', 'page']) ? 'integer' : 'string')
|
||||
: static::normalizeTypeName($type);
|
||||
}
|
||||
|
||||
[$description, $example, $enumValues, $exampleWasSpecified]
|
||||
= $this->getDescriptionAndExample($description, $type, $tagContent, $name);
|
||||
|
||||
return compact('name', 'description', 'required', 'example', 'type', 'enumValues', 'exampleWasSpecified');
|
||||
}
|
||||
}
|
||||
10
vendor/knuckleswtf/scribe/src/GroupedEndpoints/GroupedEndpointsContract.php
vendored
Normal file
10
vendor/knuckleswtf/scribe/src/GroupedEndpoints/GroupedEndpointsContract.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\GroupedEndpoints;
|
||||
|
||||
interface GroupedEndpointsContract
|
||||
{
|
||||
public function get(): array;
|
||||
|
||||
public function hasEncounteredErrors(): bool;
|
||||
}
|
||||
50
vendor/knuckleswtf/scribe/src/GroupedEndpoints/GroupedEndpointsFactory.php
vendored
Normal file
50
vendor/knuckleswtf/scribe/src/GroupedEndpoints/GroupedEndpointsFactory.php
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\GroupedEndpoints;
|
||||
|
||||
use Knuckles\Scribe\Commands\GenerateDocumentation;
|
||||
use Knuckles\Scribe\Matching\RouteMatcherInterface;
|
||||
use Knuckles\Scribe\Tools\PathConfig;
|
||||
|
||||
class GroupedEndpointsFactory
|
||||
{
|
||||
public function make(
|
||||
GenerateDocumentation $command,
|
||||
RouteMatcherInterface $routeMatcher,
|
||||
PathConfig $paths,
|
||||
): GroupedEndpointsContract {
|
||||
if ($command->isForcing()) {
|
||||
return static::fromApp(
|
||||
command: $command,
|
||||
routeMatcher: $routeMatcher,
|
||||
preserveUserChanges: false,
|
||||
paths: $paths
|
||||
);
|
||||
}
|
||||
|
||||
if ($command->shouldExtract()) {
|
||||
return static::fromApp(
|
||||
command: $command,
|
||||
routeMatcher: $routeMatcher,
|
||||
preserveUserChanges: true,
|
||||
paths: $paths
|
||||
);
|
||||
}
|
||||
|
||||
return static::fromCamelDir($paths);
|
||||
}
|
||||
|
||||
public static function fromApp(
|
||||
GenerateDocumentation $command,
|
||||
RouteMatcherInterface $routeMatcher,
|
||||
bool $preserveUserChanges,
|
||||
PathConfig $paths,
|
||||
): GroupedEndpointsFromApp {
|
||||
return new GroupedEndpointsFromApp($command, $routeMatcher, $paths, $preserveUserChanges);
|
||||
}
|
||||
|
||||
public static function fromCamelDir(PathConfig $paths): GroupedEndpointsFromCamelDir
|
||||
{
|
||||
return new GroupedEndpointsFromCamelDir($paths);
|
||||
}
|
||||
}
|
||||
309
vendor/knuckleswtf/scribe/src/GroupedEndpoints/GroupedEndpointsFromApp.php
vendored
Normal file
309
vendor/knuckleswtf/scribe/src/GroupedEndpoints/GroupedEndpointsFromApp.php
vendored
Normal file
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\GroupedEndpoints;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Camel\Camel;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Camel\Output\OutputEndpointData;
|
||||
use Knuckles\Scribe\Commands\GenerateDocumentation;
|
||||
use Knuckles\Scribe\Exceptions\CouldntGetRouteDetails;
|
||||
use Knuckles\Scribe\Extracting\ApiDetails;
|
||||
use Knuckles\Scribe\Extracting\Extractor;
|
||||
use Knuckles\Scribe\Matching\MatchedRoute;
|
||||
use Knuckles\Scribe\Matching\RouteMatcherInterface;
|
||||
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
|
||||
use Knuckles\Scribe\Tools\DocumentationConfig;
|
||||
use Knuckles\Scribe\Tools\ErrorHandlingUtils as e;
|
||||
use Knuckles\Scribe\Tools\PathConfig;
|
||||
use Knuckles\Scribe\Tools\Utils;
|
||||
use Knuckles\Scribe\Tools\Utils as u;
|
||||
use Mpociot\Reflection\DocBlock;
|
||||
use Mpociot\Reflection\DocBlock\Tag;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class GroupedEndpointsFromApp implements GroupedEndpointsContract
|
||||
{
|
||||
public static string $camelDir;
|
||||
|
||||
public static string $cacheDir;
|
||||
|
||||
private DocumentationConfig $docConfig;
|
||||
|
||||
private bool $encounteredErrors = false;
|
||||
|
||||
public function __construct(
|
||||
private GenerateDocumentation $command,
|
||||
private RouteMatcherInterface $routeMatcher,
|
||||
protected PathConfig $paths,
|
||||
private bool $preserveUserChanges = true,
|
||||
) {
|
||||
$this->docConfig = $command->getDocConfig();
|
||||
|
||||
static::$camelDir = Camel::camelDir($this->paths);
|
||||
static::$cacheDir = Camel::cacheDir($this->paths);
|
||||
}
|
||||
|
||||
public function get(): array
|
||||
{
|
||||
$groupedEndpoints = $this->extractEndpointsInfoAndWriteToDisk($this->routeMatcher, $this->preserveUserChanges);
|
||||
$this->extractAndWriteApiDetailsToDisk();
|
||||
|
||||
return $groupedEndpoints;
|
||||
}
|
||||
|
||||
public function hasEncounteredErrors(): bool
|
||||
{
|
||||
return $this->encounteredErrors;
|
||||
}
|
||||
|
||||
protected function extractEndpointsInfoAndWriteToDisk(RouteMatcherInterface $routeMatcher, bool $preserveUserChanges): array
|
||||
{
|
||||
$latestEndpointsData = [];
|
||||
$cachedEndpoints = [];
|
||||
|
||||
if ($preserveUserChanges && is_dir(static::$camelDir) && is_dir(static::$cacheDir)) {
|
||||
$latestEndpointsData = Camel::loadEndpointsToFlatPrimitivesArray(static::$camelDir);
|
||||
$cachedEndpoints = Camel::loadEndpointsToFlatPrimitivesArray(static::$cacheDir);
|
||||
}
|
||||
|
||||
$routes = $routeMatcher->getRoutes($this->docConfig->get('routes', []));
|
||||
$endpoints = $this->extractEndpointsInfoFromLaravelApp($routes, $cachedEndpoints, $latestEndpointsData);
|
||||
|
||||
$groupedEndpoints = collect($endpoints)->groupBy('metadata.groupName')->map(function (Collection $endpointsInGroup) {
|
||||
return [
|
||||
'name' => $endpointsInGroup->first(function (ExtractedEndpointData $endpointData) {
|
||||
return ! empty($endpointData->metadata->groupName);
|
||||
})->metadata->groupName ?? '',
|
||||
'description' => $endpointsInGroup->first(function (ExtractedEndpointData $endpointData) {
|
||||
return ! empty($endpointData->metadata->groupDescription);
|
||||
})->metadata->groupDescription ?? '',
|
||||
'endpoints' => $endpointsInGroup->toArray(),
|
||||
];
|
||||
})->all();
|
||||
$this->writeEndpointsToDisk($groupedEndpoints);
|
||||
|
||||
return $groupedEndpoints;
|
||||
}
|
||||
|
||||
protected function writeEndpointsToDisk(array $grouped): void
|
||||
{
|
||||
Utils::deleteFilesMatching(static::$camelDir, function ($file) {
|
||||
/** @var array|\League\Flysystem\StorageAttributes $file */
|
||||
return ! Str::startsWith(basename($file['path']), 'custom.');
|
||||
});
|
||||
Utils::deleteDirectoryAndContents(static::$cacheDir);
|
||||
|
||||
if (! is_dir(static::$camelDir)) {
|
||||
mkdir(static::$camelDir, 0o777, true);
|
||||
}
|
||||
|
||||
if (! is_dir(static::$cacheDir)) {
|
||||
mkdir(static::$cacheDir, 0o777, true);
|
||||
}
|
||||
|
||||
$fileNameIndex = 0;
|
||||
foreach ($grouped as $group) {
|
||||
$yaml = Yaml::dump(
|
||||
$group,
|
||||
20,
|
||||
2,
|
||||
Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK
|
||||
);
|
||||
|
||||
// Format numbers as two digits so they are sorted properly when retrieving later
|
||||
// (ie "10.yaml" comes after "9.yaml", not after "1.yaml")
|
||||
$fileName = sprintf('%02d.yaml', $fileNameIndex);
|
||||
$fileNameIndex++;
|
||||
|
||||
file_put_contents(static::$camelDir."/{$fileName}", $yaml);
|
||||
file_put_contents(static::$cacheDir."/{$fileName}", "## Autogenerated by Scribe. DO NOT MODIFY.\n\n".$yaml);
|
||||
}
|
||||
}
|
||||
|
||||
protected function extractAndWriteApiDetailsToDisk(): void
|
||||
{
|
||||
$apiDetails = $this->makeApiDetails();
|
||||
|
||||
$apiDetails->writeMarkdownFiles();
|
||||
}
|
||||
|
||||
protected function makeApiDetails(): ApiDetails
|
||||
{
|
||||
return new ApiDetails($this->paths, $this->docConfig, ! $this->command->option('force'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a new extractor.
|
||||
*/
|
||||
protected function makeExtractor(): Extractor
|
||||
{
|
||||
return new Extractor($this->docConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MatchedRoute[] $matches
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function extractEndpointsInfoFromLaravelApp(array $matches, array $cachedEndpoints, array $latestEndpointsData): array
|
||||
{
|
||||
$extractor = $this->makeExtractor();
|
||||
|
||||
$parsedEndpoints = [];
|
||||
|
||||
foreach ($matches as $routeItem) {
|
||||
$route = $routeItem->getRoute();
|
||||
|
||||
$routeControllerAndMethod = u::getRouteClassAndMethodNames($route);
|
||||
if (! $this->isValidRoute($routeControllerAndMethod)) {
|
||||
c::warn('Skipping invalid route: '.c::getRouteRepresentation($route));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $this->doesControllerMethodExist($routeControllerAndMethod)) {
|
||||
c::warn('Skipping route: '.c::getRouteRepresentation($route).' - Controller method does not exist.');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isRouteHiddenFromDocumentation($routeControllerAndMethod)) {
|
||||
c::warn('Skipping route: '.c::getRouteRepresentation($route).': @hideFromAPIDocumentation was specified.');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Start buffering warnings so they don't break the task output.
|
||||
c::startWarningBuffer();
|
||||
|
||||
$this->command->outputComponents()->task(
|
||||
c::getRouteRepresentation($route),
|
||||
function () use ($extractor, $route, $routeItem, $cachedEndpoints, $latestEndpointsData, &$parsedEndpoints) {
|
||||
$currentEndpointData = $extractor->processRoute($route, $routeItem->getRules());
|
||||
// If latest data is different from cached data, merge latest into current.
|
||||
$currentEndpointData = $this->mergeAnyEndpointDataUpdates($currentEndpointData, $cachedEndpoints, $latestEndpointsData);
|
||||
$parsedEndpoints[] = $currentEndpointData;
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
// Flush buffered warnings after a task completes.
|
||||
c::flushWarningBuffer();
|
||||
} catch (\Exception $exception) {
|
||||
$this->encounteredErrors = true;
|
||||
c::flushWarningBuffer(); // Flush warnings even in error.
|
||||
e::dumpExceptionIfVerbose($exception);
|
||||
}
|
||||
}
|
||||
|
||||
return $parsedEndpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array[] $cachedEndpoints
|
||||
* @param array[] $latestEndpointsData
|
||||
* @return ExtractedEndpointData The extracted endpoint data
|
||||
*/
|
||||
private function mergeAnyEndpointDataUpdates(ExtractedEndpointData $endpointData, array $cachedEndpoints, array $latestEndpointsData): ExtractedEndpointData
|
||||
{
|
||||
// First, find the corresponding endpoint in cached and latest
|
||||
$thisEndpointCached = Arr::first($cachedEndpoints, function (array $endpoint) use ($endpointData) {
|
||||
return $endpoint['uri'] === $endpointData->uri && $endpoint['httpMethods'] === $endpointData->httpMethods;
|
||||
});
|
||||
if (! $thisEndpointCached) {
|
||||
return $endpointData;
|
||||
}
|
||||
|
||||
$thisEndpointLatest = Arr::first($latestEndpointsData, function (array $endpoint) use ($endpointData) {
|
||||
return $endpoint['uri'] === $endpointData->uri && $endpoint['httpMethods'] === $endpointData->httpMethods;
|
||||
});
|
||||
if (! $thisEndpointLatest) {
|
||||
return $endpointData;
|
||||
}
|
||||
|
||||
// Then compare cached and latest to see what sections changed.
|
||||
$properties = [
|
||||
'metadata',
|
||||
'headers',
|
||||
'urlParameters',
|
||||
'queryParameters',
|
||||
'bodyParameters',
|
||||
'responses',
|
||||
'responseFields',
|
||||
];
|
||||
|
||||
$changed = [];
|
||||
foreach ($properties as $property) {
|
||||
if ($thisEndpointCached[$property] !== $thisEndpointLatest[$property]) {
|
||||
$changed[] = $property;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, merge any changed sections.
|
||||
$thisEndpointLatest = OutputEndpointData::create($thisEndpointLatest);
|
||||
foreach ($changed as $property) {
|
||||
$endpointData->{$property} = $thisEndpointLatest->{$property};
|
||||
}
|
||||
|
||||
return $endpointData;
|
||||
}
|
||||
|
||||
private function isValidRoute(?array $routeControllerAndMethod): bool
|
||||
{
|
||||
if (is_array($routeControllerAndMethod)) {
|
||||
if (count($routeControllerAndMethod) < 2) {
|
||||
throw CouldntGetRouteDetails::new();
|
||||
}
|
||||
[$classOrObject, $method] = $routeControllerAndMethod;
|
||||
if (u::isInvokableObject($classOrObject)) {
|
||||
return true;
|
||||
}
|
||||
$routeControllerAndMethod = $classOrObject.'@'.$method;
|
||||
}
|
||||
|
||||
return ! is_callable($routeControllerAndMethod) && ! is_null($routeControllerAndMethod);
|
||||
}
|
||||
|
||||
private function doesControllerMethodExist(array $routeControllerAndMethod): bool
|
||||
{
|
||||
if (count($routeControllerAndMethod) < 2) {
|
||||
throw CouldntGetRouteDetails::new();
|
||||
}
|
||||
[$class, $method] = $routeControllerAndMethod;
|
||||
$reflection = new \ReflectionClass($class);
|
||||
|
||||
if ($reflection->hasMethod($method)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isRouteHiddenFromDocumentation(array $routeControllerAndMethod): bool
|
||||
{
|
||||
if (! ($class = $routeControllerAndMethod[0]) instanceof \Closure) {
|
||||
$classDocBlock = new DocBlock((new \ReflectionClass($class))->getDocComment() ?: '');
|
||||
$shouldIgnoreClass = collect($classDocBlock->getTags())
|
||||
->filter(function (Tag $tag) {
|
||||
return Str::lower($tag->getName()) === 'hidefromapidocumentation';
|
||||
})->isNotEmpty();
|
||||
|
||||
if ($shouldIgnoreClass) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$methodDocBlock = new DocBlock(u::getReflectedRouteMethod($routeControllerAndMethod)->getDocComment() ?: '');
|
||||
$shouldIgnoreMethod = collect($methodDocBlock->getTags())
|
||||
->filter(function (Tag $tag) {
|
||||
return Str::lower($tag->getName()) === 'hidefromapidocumentation';
|
||||
})->isNotEmpty();
|
||||
|
||||
return $shouldIgnoreMethod;
|
||||
}
|
||||
}
|
||||
27
vendor/knuckleswtf/scribe/src/GroupedEndpoints/GroupedEndpointsFromCamelDir.php
vendored
Normal file
27
vendor/knuckleswtf/scribe/src/GroupedEndpoints/GroupedEndpointsFromCamelDir.php
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\GroupedEndpoints;
|
||||
|
||||
use Knuckles\Camel\Camel;
|
||||
use Knuckles\Scribe\Tools\PathConfig;
|
||||
|
||||
class GroupedEndpointsFromCamelDir implements GroupedEndpointsContract
|
||||
{
|
||||
public function __construct(protected PathConfig $paths) {}
|
||||
|
||||
public function get(): array
|
||||
{
|
||||
if (! is_dir(Camel::camelDir($this->paths))) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Can't use --no-extraction because there are no endpoints in the ".Camel::camelDir($this->paths).' directory.'
|
||||
);
|
||||
}
|
||||
|
||||
return Camel::loadEndpointsIntoGroups(Camel::camelDir($this->paths));
|
||||
}
|
||||
|
||||
public function hasEncounteredErrors(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
50
vendor/knuckleswtf/scribe/src/Matching/MatchedRoute.php
vendored
Normal file
50
vendor/knuckleswtf/scribe/src/Matching/MatchedRoute.php
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Matching;
|
||||
|
||||
use Illuminate\Routing\Route;
|
||||
|
||||
class MatchedRoute implements \ArrayAccess
|
||||
{
|
||||
protected Route $route;
|
||||
|
||||
/** @deprecated Use the strategy config instead */
|
||||
protected array $rules;
|
||||
|
||||
public function __construct(Route $route, array $applyRules = [])
|
||||
{
|
||||
$this->route = $route;
|
||||
$this->rules = $applyRules;
|
||||
}
|
||||
|
||||
public function getRoute(): Route
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/** @deprecated Use the strategy config instead */
|
||||
public function getRules(): array
|
||||
{
|
||||
return $this->rules;
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return is_callable([$this, 'get'.ucfirst($offset)]);
|
||||
}
|
||||
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return call_user_func([$this, 'get'.ucfirst($offset)]);
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$this->{$offset} = $value;
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
$this->{$offset} = null;
|
||||
}
|
||||
}
|
||||
72
vendor/knuckleswtf/scribe/src/Matching/RouteMatcher.php
vendored
Normal file
72
vendor/knuckleswtf/scribe/src/Matching/RouteMatcher.php
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Matching;
|
||||
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Scribe\Tools\RoutePatternMatcher;
|
||||
|
||||
class RouteMatcher implements RouteMatcherInterface
|
||||
{
|
||||
public function getRoutes(array $routeRules = []): array
|
||||
{
|
||||
return $this->getRoutesToBeDocumented($routeRules);
|
||||
}
|
||||
|
||||
private function getRoutesToBeDocumented(array $routeRules): array
|
||||
{
|
||||
$allRoutes = $this->getAllRoutes();
|
||||
|
||||
$matchedRoutes = [];
|
||||
|
||||
foreach ($routeRules as $routeRule) {
|
||||
$includes = $routeRule['include'] ?? [];
|
||||
|
||||
foreach ($allRoutes as $route) {
|
||||
if ($this->shouldExcludeRoute($route, $routeRule)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->shouldIncludeRoute($route, $routeRule, $includes)) {
|
||||
$matchedRoutes[] = new MatchedRoute($route, $routeRule['apply'] ?? []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $matchedRoutes;
|
||||
}
|
||||
|
||||
private function getAllRoutes()
|
||||
{
|
||||
return RouteFacade::getRoutes();
|
||||
}
|
||||
|
||||
private function shouldIncludeRoute(Route $route, array $routeRule, array $mustIncludes): bool
|
||||
{
|
||||
if (RoutePatternMatcher::matches($route, $mustIncludes)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$domainsToMatch = $routeRule['match']['domains'] ?? [];
|
||||
$pathsToMatch = $routeRule['match']['prefixes'] ?? [];
|
||||
|
||||
return Str::is($domainsToMatch, $route->getDomain()) && Str::is($pathsToMatch, $route->uri());
|
||||
}
|
||||
|
||||
private function shouldExcludeRoute(Route $route, array $routeRule): bool
|
||||
{
|
||||
$excludes = $routeRule['exclude'] ?? [];
|
||||
|
||||
// Exclude this package's routes
|
||||
$excludes[] = 'scribe';
|
||||
$excludes[] = 'scribe.*';
|
||||
|
||||
// Exclude Laravel Telescope routes
|
||||
if (class_exists('Laravel\Telescope\Telescope')) {
|
||||
$excludes[] = 'telescope/*';
|
||||
}
|
||||
|
||||
return RoutePatternMatcher::matches($route, $excludes);
|
||||
}
|
||||
}
|
||||
14
vendor/knuckleswtf/scribe/src/Matching/RouteMatcherInterface.php
vendored
Normal file
14
vendor/knuckleswtf/scribe/src/Matching/RouteMatcherInterface.php
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Matching;
|
||||
|
||||
interface RouteMatcherInterface
|
||||
{
|
||||
/**
|
||||
* Resolve matched routes that should be documented.
|
||||
*
|
||||
* @param array $routeRules Route rules defined under the "routes" section in config
|
||||
* @return MatchedRoute[]
|
||||
*/
|
||||
public function getRoutes(array $routeRules = []): array;
|
||||
}
|
||||
98
vendor/knuckleswtf/scribe/src/Scribe.php
vendored
Normal file
98
vendor/knuckleswtf/scribe/src/Scribe.php
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe;
|
||||
|
||||
use Illuminate\Routing\Route;
|
||||
use Knuckles\Camel\Extraction\ExtractedEndpointData;
|
||||
use Knuckles\Scribe\Commands\GenerateDocumentation;
|
||||
use Knuckles\Scribe\Tools\Globals;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Scribe
|
||||
{
|
||||
public const VERSION = '5.9.0';
|
||||
|
||||
/**
|
||||
* Specify a callback that will be executed just before a response call is made
|
||||
* (after configuring the environment and starting a transaction).
|
||||
*
|
||||
* @param callable(Request, ExtractedEndpointData): mixed $callable
|
||||
*/
|
||||
public static function beforeResponseCall(callable $callable)
|
||||
{
|
||||
Globals::$__beforeResponseCall = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a callback that will be executed just after a response call is done
|
||||
* (allowing to modify the response).
|
||||
*
|
||||
* @param callable(Request, ExtractedEndpointData, mixed): mixed $callable
|
||||
*/
|
||||
public static function afterResponseCall(callable $callable)
|
||||
{
|
||||
Globals::$__afterResponseCall = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a callback that will be executed just before the generate command is executed.
|
||||
*
|
||||
* @param callable(GenerateDocumentation): mixed $callable
|
||||
*/
|
||||
public static function bootstrap(callable $callable)
|
||||
{
|
||||
Globals::$__bootstrap = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a callback that will be executed when Scribe is done generating your docs.
|
||||
* This callback will receive a map of all the output paths generated, that looks like this:
|
||||
* [
|
||||
* 'postman' => '/absolute/path/to/postman/collection',
|
||||
* 'openapi' => '/absolute/path/to/openapi/spec',
|
||||
* // If you're using `laravel` type, `html` will be null, and vice versa for `blade`.
|
||||
* 'html' => '/absolute/path/to/index.html/',
|
||||
* 'blade' => '/absolute/path/to/blade/view',
|
||||
* // These are paths to asset folders
|
||||
* 'assets' => [
|
||||
* 'js' => '/path/to/js/assets/folder',
|
||||
* 'css' => '/path/to/css/assets/folder',
|
||||
* 'images' => '/path/to/images/assets/folder',
|
||||
* ]
|
||||
* ].
|
||||
*
|
||||
* If you disabled `postman` or `openapi`, their values will be null.
|
||||
*
|
||||
* @param callable(array): mixed $callable
|
||||
*/
|
||||
public static function afterGenerating(callable $callable)
|
||||
{
|
||||
Globals::$__afterGenerating = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a callback that will be used by all FormRequest strategies
|
||||
* to instantiate Form Requests. his callback takes the name of the form request class,
|
||||
* the current Laravel route being processed, and the controller method.
|
||||
*
|
||||
* @param ?callable(string,Route,\ReflectionFunctionAbstract): mixed $callable
|
||||
*/
|
||||
public static function instantiateFormRequestUsing(?callable $callable)
|
||||
{
|
||||
Globals::$__instantiateFormRequestUsing = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a callback that will be called when instantiating an `ExtractedEndpointData` object
|
||||
* in order to normalize the URL. The default normalization tries to convert URL parameters from
|
||||
* Laravel resource-style (`users/{user}/projects/{project}`)
|
||||
* to a general style (`users/{user_id}/projects/{id}`).
|
||||
* The callback will be passed the default Laravel URL, the route object, the controller method and class.
|
||||
*
|
||||
* @param ?callable(string,Route,\ReflectionFunctionAbstract,?\ReflectionClass): string $callable
|
||||
*/
|
||||
public static function normalizeEndpointUrlUsing(?callable $callable)
|
||||
{
|
||||
Globals::$__normalizeEndpointUrlUsing = $callable;
|
||||
}
|
||||
}
|
||||
119
vendor/knuckleswtf/scribe/src/ScribeServiceProvider.php
vendored
Normal file
119
vendor/knuckleswtf/scribe/src/ScribeServiceProvider.php
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Scribe\Commands\DiffConfig;
|
||||
use Knuckles\Scribe\Commands\GenerateDocumentation;
|
||||
use Knuckles\Scribe\Commands\MakeStrategy;
|
||||
use Knuckles\Scribe\Matching\RouteMatcher;
|
||||
use Knuckles\Scribe\Matching\RouteMatcherInterface;
|
||||
use Knuckles\Scribe\Tools\BladeMarkdownEngine;
|
||||
use Knuckles\Scribe\Writing\CustomTranslationsLoader;
|
||||
|
||||
class ScribeServiceProvider extends ServiceProvider
|
||||
{
|
||||
public static bool $customTranslationLayerLoaded = false;
|
||||
|
||||
public function boot()
|
||||
{
|
||||
$this->registerViews();
|
||||
|
||||
$this->registerConfig();
|
||||
|
||||
$this->bootRoutes();
|
||||
|
||||
$this->registerCommands();
|
||||
|
||||
$this->configureTranslations();
|
||||
|
||||
// Bind the route matcher implementation
|
||||
$this->app->bind(RouteMatcherInterface::class, config('scribe.routeMatcher', RouteMatcher::class));
|
||||
|
||||
if (! class_exists('Str')) {
|
||||
// We don't want to have to use the FQN in our blade files.
|
||||
class_alias(Str::class, 'Str');
|
||||
}
|
||||
}
|
||||
|
||||
// Allows our custom translation layer to be loaded on demand,
|
||||
// so we minimize issues with interference from framework/package/environment.
|
||||
// ALso, Laravel's `app->runningInConsole()` isn't reliable enough. See issue #676
|
||||
public function loadCustomTranslationLayer(): void
|
||||
{
|
||||
$this->app->extend('translation.loader', function ($defaultFileLoader) {
|
||||
return app(CustomTranslationsLoader::class, ['loader' => $defaultFileLoader]);
|
||||
});
|
||||
$this->app->forgetInstance('translator');
|
||||
self::$customTranslationLayerLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add docs routes for users that want their docs to pass through their Laravel app.
|
||||
*/
|
||||
protected function bootRoutes()
|
||||
{
|
||||
$docsType = config('scribe.type', 'laravel');
|
||||
if (Str::endsWith($docsType, 'laravel') && config('scribe.laravel.add_routes', true)) {
|
||||
$routesPath = __DIR__.'/../routes/laravel.php';
|
||||
$this->loadRoutesFrom($routesPath);
|
||||
}
|
||||
}
|
||||
|
||||
protected function configureTranslations(): void
|
||||
{
|
||||
$this->publishes([
|
||||
__DIR__.'/../lang/' => $this->app->langPath(),
|
||||
], 'scribe-translations');
|
||||
|
||||
$this->loadTranslationsFrom($this->app->langPath('scribe.php'), 'scribe');
|
||||
$this->loadTranslationsFrom(realpath(__DIR__.'/../lang'), 'scribe');
|
||||
}
|
||||
|
||||
protected function registerViews(): void
|
||||
{
|
||||
// Register custom Markdown Blade compiler so we can automatically have MD views converted to HTML
|
||||
$this->app->view->getEngineResolver()
|
||||
->register('blademd', fn () => new BladeMarkdownEngine($this->app['blade.compiler']));
|
||||
$this->app->view->addExtension('md.blade.php', 'blademd');
|
||||
|
||||
$this->loadViewsFrom(__DIR__.'/../resources/views/', 'scribe');
|
||||
|
||||
// Publish views in separate, smaller groups for ease of end-user modifications
|
||||
$viewGroups = [
|
||||
'views' => '',
|
||||
'examples' => 'partials/example-requests',
|
||||
'themes' => 'themes',
|
||||
'markdown' => 'markdown',
|
||||
'external' => 'external',
|
||||
];
|
||||
foreach ($viewGroups as $group => $path) {
|
||||
$this->publishes([
|
||||
__DIR__."/../resources/views/{$path}" => $this->app->basePath("resources/views/vendor/scribe/{$path}"),
|
||||
], "scribe-{$group}");
|
||||
}
|
||||
}
|
||||
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$this->publishes([
|
||||
__DIR__.'/../config/scribe.php' => $this->app->configPath('scribe.php'),
|
||||
], 'scribe-config');
|
||||
|
||||
$this->mergeConfigFrom(__DIR__.'/../config/scribe.php', 'scribe');
|
||||
}
|
||||
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
GenerateDocumentation::class,
|
||||
MakeStrategy::class,
|
||||
// Retired for the same reasons as the upgrade check
|
||||
// Upgrade::class,
|
||||
DiffConfig::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
vendor/knuckleswtf/scribe/src/Tools/AnnotationParser.php
vendored
Normal file
57
vendor/knuckleswtf/scribe/src/Tools/AnnotationParser.php
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tools;
|
||||
|
||||
class AnnotationParser
|
||||
{
|
||||
/**
|
||||
* Parse an annotation like 'status=400 when="things go wrong" {"message": "failed"}'.
|
||||
* Fields are always optional and may appear at the start or the end of the string.
|
||||
*
|
||||
* @param array $allowedFields list of fields to look for
|
||||
* @return array{content: string, fields: string[]}
|
||||
*/
|
||||
public static function parseIntoContentAndFields(string $annotationContent, array $allowedFields): array
|
||||
{
|
||||
$parsedFields = array_fill_keys($allowedFields, null);
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
preg_match("/{$field}=([^\\s'\"]+|\".+?\"|'.+?')\\s*/", $annotationContent, $fieldAndValue);
|
||||
|
||||
if (count($fieldAndValue)) {
|
||||
[$matchingText, $attributeValue] = $fieldAndValue;
|
||||
$annotationContent = str_replace($matchingText, '', $annotationContent);
|
||||
|
||||
$parsedFields[$field] = mb_trim($attributeValue, '"\' ');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => mb_trim($annotationContent),
|
||||
'fields' => $parsedFields,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an annotation like 'title=This message="everything good"' into a key-value array.
|
||||
* All non key-value fields will be ignored. Useful for `@apiResourceAdditional`,
|
||||
* where users may specify arbitrary fields.
|
||||
*/
|
||||
public static function parseIntoFields(string $annotationContent): array
|
||||
{
|
||||
$fields = $matches = [];
|
||||
|
||||
preg_match_all(
|
||||
'/([^\s\'"]+|".+?"|\'.+?\')=([^\s\'"]+|".+?"|\'.+?\')/',
|
||||
$annotationContent,
|
||||
$matches,
|
||||
PREG_SET_ORDER,
|
||||
);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$fields[mb_trim($match[1], '"\' ')] = mb_trim($match[2], '"\' ');
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
30
vendor/knuckleswtf/scribe/src/Tools/BladeMarkdownEngine.php
vendored
Normal file
30
vendor/knuckleswtf/scribe/src/Tools/BladeMarkdownEngine.php
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tools;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\View\Compilers\CompilerInterface;
|
||||
use Illuminate\View\Engines\CompilerEngine;
|
||||
|
||||
class BladeMarkdownEngine extends CompilerEngine
|
||||
{
|
||||
private \Parsedown $markdown;
|
||||
|
||||
public function __construct(CompilerInterface $compiler, ?Filesystem $files = null)
|
||||
{
|
||||
parent::__construct($compiler, $files ?: new Filesystem);
|
||||
$this->markdown = \Parsedown::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the evaluated contents of the view.
|
||||
*
|
||||
* @param mixed $path
|
||||
*/
|
||||
public function get($path, array $data = [])
|
||||
{
|
||||
$contents = parent::get($path, $data);
|
||||
|
||||
return $this->markdown->text($contents);
|
||||
}
|
||||
}
|
||||
99
vendor/knuckleswtf/scribe/src/Tools/ConfigDiffer.php
vendored
Normal file
99
vendor/knuckleswtf/scribe/src/Tools/ConfigDiffer.php
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tools;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\VarExporter\VarExporter;
|
||||
|
||||
class ConfigDiffer
|
||||
{
|
||||
public function __construct(
|
||||
protected array $original,
|
||||
protected array $changed,
|
||||
protected array $ignorePaths = [],
|
||||
protected array $asList = [],
|
||||
) {}
|
||||
|
||||
public function getDiff()
|
||||
{
|
||||
return $this->recursiveItemDiff($this->original, $this->changed);
|
||||
}
|
||||
|
||||
protected function recursiveItemDiff($old, $new, $prefix = '')
|
||||
{
|
||||
$diff = [];
|
||||
|
||||
foreach ($new as $key => $value) {
|
||||
$fullKey = $prefix.$key;
|
||||
if (Str::is($this->ignorePaths, $fullKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldValue = data_get($old, $key);
|
||||
|
||||
if (is_array($value)) {
|
||||
if (Str::is($this->asList, $fullKey)) {
|
||||
$listDiff = $this->diffList($oldValue, $value);
|
||||
if (! empty($listDiff)) {
|
||||
$diff[$fullKey] = $listDiff;
|
||||
}
|
||||
} else {
|
||||
$diff = array_merge(
|
||||
$diff,
|
||||
$this->recursiveItemDiff($oldValue, $value, "{$fullKey}.")
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if ($oldValue !== $value) {
|
||||
$printedValue = json_encode($value, JSON_UNESCAPED_SLASHES);
|
||||
$diff[$prefix.$key] = $printedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
protected function diffList(mixed $oldValue, array $value)
|
||||
{
|
||||
if (! is_array($oldValue)) {
|
||||
return 'changed to a list';
|
||||
}
|
||||
|
||||
$added = array_map(fn ($v) => "{$v}", $this->subtractArraysFlat($value, $oldValue));
|
||||
$removed = array_map(fn ($v) => "{$v}", $this->subtractArraysFlat($oldValue, $value));
|
||||
|
||||
$diff = [];
|
||||
if (! empty($added)) {
|
||||
$diff[] = 'added '.implode(', ', $added);
|
||||
}
|
||||
if (! empty($removed)) {
|
||||
$diff[] = 'removed '.implode(', ', $removed);
|
||||
}
|
||||
|
||||
return empty($diff) ? '' : implode(': ', $diff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basically array_diff, but handling items which may also be arrays.
|
||||
*/
|
||||
protected function subtractArraysFlat(array $a, array $b)
|
||||
{
|
||||
$mapped_a = array_map(function ($item) {
|
||||
if (is_array($item)) {
|
||||
return VarExporter::export($item);
|
||||
}
|
||||
|
||||
return $item;
|
||||
}, $a);
|
||||
$mapped_b = array_map(function ($item) {
|
||||
if (is_array($item)) {
|
||||
return VarExporter::export($item);
|
||||
}
|
||||
|
||||
return $item;
|
||||
}, $b);
|
||||
|
||||
return array_diff($mapped_a, $mapped_b);
|
||||
}
|
||||
}
|
||||
174
vendor/knuckleswtf/scribe/src/Tools/ConsoleOutputUtils.php
vendored
Normal file
174
vendor/knuckleswtf/scribe/src/Tools/ConsoleOutputUtils.php
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tools;
|
||||
|
||||
use Illuminate\Routing\Route;
|
||||
use Knuckles\Scribe\Commands\GenerateDocumentation;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ConsoleOutputUtils
|
||||
{
|
||||
/**
|
||||
* @var null|OutputInterface
|
||||
*/
|
||||
private static $output;
|
||||
|
||||
/**
|
||||
* @var null|GenerateDocumentation
|
||||
*/
|
||||
private static $command;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private static $warningBuffer = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $isBufferingWarnings = false;
|
||||
|
||||
public static function bootstrapOutput(OutputInterface $outputInterface): void
|
||||
{
|
||||
self::$output = $outputInterface;
|
||||
}
|
||||
|
||||
public static function setCommand(?GenerateDocumentation $command): void
|
||||
{
|
||||
self::$command = $command;
|
||||
}
|
||||
|
||||
public static function startWarningBuffer(): void
|
||||
{
|
||||
self::$isBufferingWarnings = true;
|
||||
self::$warningBuffer = [];
|
||||
}
|
||||
|
||||
public static function flushWarningBuffer(): void
|
||||
{
|
||||
self::$isBufferingWarnings = false;
|
||||
foreach (self::$warningBuffer as $warning) {
|
||||
self::warn($warning);
|
||||
}
|
||||
self::$warningBuffer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task with Laravel-style output (dots, timing, status).
|
||||
*/
|
||||
public static function task(string $description, callable $task): mixed
|
||||
{
|
||||
if (self::$command) {
|
||||
// Laravel's task() method doesn't return the callback result, so we capture it.
|
||||
$result = null;
|
||||
self::$command->outputComponents()->task($description, function () use (&$result, $task) {
|
||||
$result = $task();
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Fallback for contexts without command
|
||||
self::info($description);
|
||||
$result = $task();
|
||||
self::success($description);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function deprecated(string $feature, string $inVersion, ?string $should = null): void
|
||||
{
|
||||
$message = "You're using {$feature}. This is deprecated and will be removed in the next major version.";
|
||||
if ($should) {
|
||||
$message .= "\nYou should {$should} instead.";
|
||||
}
|
||||
$message .= " See the changelog for details (v{$inVersion}).";
|
||||
|
||||
self::warn($message);
|
||||
}
|
||||
|
||||
public static function warn(string $message): void
|
||||
{
|
||||
if (self::$isBufferingWarnings) {
|
||||
self::$warningBuffer[] = $message;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::$command) {
|
||||
self::$command->outputComponents()->warn($message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! self::$output) {
|
||||
self::bootstrapOutput(new ConsoleOutput);
|
||||
}
|
||||
self::$output->writeln("<fg=yellow> ⚠ {$message}</>");
|
||||
}
|
||||
|
||||
public static function info(string $message): void
|
||||
{
|
||||
if (self::$command) {
|
||||
self::$command->outputComponents()->info($message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! self::$output) {
|
||||
self::bootstrapOutput(new ConsoleOutput);
|
||||
}
|
||||
self::$output->writeln("<fg=gray> ℹ {$message}</>");
|
||||
}
|
||||
|
||||
public static function debug(string $message): void
|
||||
{
|
||||
if (! Globals::$shouldBeVerbose) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! self::$output) {
|
||||
self::bootstrapOutput(new ConsoleOutput);
|
||||
}
|
||||
self::$output->writeln("<fg=gray> 🐛 {$message}</>");
|
||||
}
|
||||
|
||||
public static function success(string $message): void
|
||||
{
|
||||
if (! self::$output) {
|
||||
self::bootstrapOutput(new ConsoleOutput);
|
||||
}
|
||||
self::$output->writeln("<fg=green> ✔ {$message}</>");
|
||||
}
|
||||
|
||||
public static function error(string $message): void
|
||||
{
|
||||
if (self::$command) {
|
||||
self::$command->outputComponents()->error($message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! self::$output) {
|
||||
self::bootstrapOutput(new ConsoleOutput);
|
||||
}
|
||||
self::$output->writeln("<fg=red> ✖ {$message}</>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string representation of a route to output to the console eg [GET] /api/users.
|
||||
*/
|
||||
public static function getRouteRepresentation(Route $route): string
|
||||
{
|
||||
$methods = $route->methods();
|
||||
if (count($methods) > 1) {
|
||||
$methods = array_diff($route->methods(), ['HEAD']);
|
||||
}
|
||||
|
||||
$routeMethods = implode('|', $methods);
|
||||
$routePath = $route->uri();
|
||||
|
||||
return "[<fg=cyan>{$routeMethods}</>] {$routePath}";
|
||||
}
|
||||
}
|
||||
42
vendor/knuckleswtf/scribe/src/Tools/DocumentationConfig.php
vendored
Normal file
42
vendor/knuckleswtf/scribe/src/Tools/DocumentationConfig.php
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tools;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DocumentationConfig
|
||||
{
|
||||
public array $data;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->data = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a config item with dot notation.
|
||||
* If the key does not exist, $default (or null) will be returned.
|
||||
*
|
||||
* @param mixed $default
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function get(string $key, $default = null)
|
||||
{
|
||||
return data_get($this->data, $key, $default);
|
||||
}
|
||||
|
||||
public function outputIsStatic(): bool
|
||||
{
|
||||
return ! $this->outputRoutedThroughLaravel();
|
||||
}
|
||||
|
||||
public function outputRoutedThroughLaravel(): bool
|
||||
{
|
||||
return Str::is(['laravel', 'external_laravel'], $this->get('type'));
|
||||
}
|
||||
|
||||
public function outputIsExternal(): bool
|
||||
{
|
||||
return Str::is(['external_static', 'external_laravel'], $this->get('type'));
|
||||
}
|
||||
}
|
||||
42
vendor/knuckleswtf/scribe/src/Tools/ErrorHandlingUtils.php
vendored
Normal file
42
vendor/knuckleswtf/scribe/src/Tools/ErrorHandlingUtils.php
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tools;
|
||||
|
||||
use NunoMaduro\Collision\Handler;
|
||||
use NunoMaduro\Collision\Writer;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Whoops\Exception\Inspector;
|
||||
|
||||
class ErrorHandlingUtils
|
||||
{
|
||||
public static function dumpExceptionIfVerbose(\Throwable $e): void
|
||||
{
|
||||
if (Globals::$shouldBeVerbose) {
|
||||
self::dumpException($e);
|
||||
|
||||
return;
|
||||
}
|
||||
[$firstFrame, $secondFrame] = $e->getTrace();
|
||||
|
||||
try {
|
||||
['file' => $file, 'line' => $line] = $firstFrame;
|
||||
} catch (\Exception $_) {
|
||||
['file' => $file, 'line' => $line] = $secondFrame;
|
||||
}
|
||||
$exceptionType = get_class($e);
|
||||
$message = $e->getMessage();
|
||||
$message = "{$exceptionType} in {$file} at line {$line}: {$message}";
|
||||
ConsoleOutputUtils::error($message);
|
||||
ConsoleOutputUtils::error('Run this again with the --verbose flag to see the full stack trace.');
|
||||
}
|
||||
|
||||
public static function dumpException(\Throwable $e): void
|
||||
{
|
||||
$output = new ConsoleOutput(OutputInterface::VERBOSITY_VERBOSE);
|
||||
$handler = new Handler(new Writer(null, $output));
|
||||
$handler->setInspector(new Inspector($e));
|
||||
$handler->setException($e);
|
||||
$handler->handle();
|
||||
}
|
||||
}
|
||||
22
vendor/knuckleswtf/scribe/src/Tools/Globals.php
vendored
Normal file
22
vendor/knuckleswtf/scribe/src/Tools/Globals.php
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tools;
|
||||
|
||||
class Globals
|
||||
{
|
||||
public static bool $shouldBeVerbose = false;
|
||||
|
||||
// Hooks, used by users to configure Scribe's behaviour.
|
||||
|
||||
public static $__beforeResponseCall;
|
||||
|
||||
public static $__afterResponseCall;
|
||||
|
||||
public static $__bootstrap;
|
||||
|
||||
public static $__afterGenerating;
|
||||
|
||||
public static $__instantiateFormRequestUsing;
|
||||
|
||||
public static $__normalizeEndpointUrlUsing;
|
||||
}
|
||||
31
vendor/knuckleswtf/scribe/src/Tools/MarkdownParser.php
vendored
Normal file
31
vendor/knuckleswtf/scribe/src/Tools/MarkdownParser.php
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tools;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MarkdownParser extends \Parsedown
|
||||
{
|
||||
public array $headings = [];
|
||||
|
||||
protected function blockHeader($Line)
|
||||
{
|
||||
$block = parent::blockHeader($Line);
|
||||
if (isset($block['element']['name'])) {
|
||||
$text = $block['element']['text']
|
||||
?? $block['element']['handler']['argument']
|
||||
?? '';
|
||||
$text = is_string($text) ? $text : '';
|
||||
$level = (int) mb_trim($block['element']['name'], 'h');
|
||||
$slug = Str::slug($text);
|
||||
$block['element']['attributes']['id'] = $slug;
|
||||
$this->headings[] = [
|
||||
'text' => $text,
|
||||
'level' => $level,
|
||||
'slug' => $slug,
|
||||
];
|
||||
}
|
||||
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
46
vendor/knuckleswtf/scribe/src/Tools/PathConfig.php
vendored
Normal file
46
vendor/knuckleswtf/scribe/src/Tools/PathConfig.php
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tools;
|
||||
|
||||
/**
|
||||
* A home for path configurations. The important paths Scribe depends on.
|
||||
*/
|
||||
class PathConfig
|
||||
{
|
||||
public function __construct(
|
||||
public string $configName = 'scribe',
|
||||
// FOr lack of a better name, we'll call this `scribeDir`.
|
||||
// It's sort of the cache dir, where Scribe stores its intermediate outputs.
|
||||
protected ?string $scribeDir = null,
|
||||
) {
|
||||
if (is_null($this->scribeDir)) {
|
||||
$this->scribeDir = ".{$this->configName}";
|
||||
}
|
||||
}
|
||||
|
||||
public function outputPath(?string $resolvePath = null, string $separator = '/'): string
|
||||
{
|
||||
if (is_null($resolvePath)) {
|
||||
return $this->configName;
|
||||
}
|
||||
|
||||
return "{$this->configName}{$separator}{$resolvePath}";
|
||||
}
|
||||
|
||||
public function configFileName(): string
|
||||
{
|
||||
return "{$this->configName}.php";
|
||||
}
|
||||
|
||||
/**
|
||||
* The directory where Scribe writes its intermediate output (default is .<config> ie .scribe).
|
||||
*/
|
||||
public function intermediateOutputPath(?string $resolvePath = null, string $separator = '/'): string
|
||||
{
|
||||
if (is_null($resolvePath)) {
|
||||
return $this->scribeDir;
|
||||
}
|
||||
|
||||
return "{$this->scribeDir}{$separator}{$resolvePath}";
|
||||
}
|
||||
}
|
||||
31
vendor/knuckleswtf/scribe/src/Tools/RoutePatternMatcher.php
vendored
Normal file
31
vendor/knuckleswtf/scribe/src/Tools/RoutePatternMatcher.php
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Scribe\Tools;
|
||||
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RoutePatternMatcher
|
||||
{
|
||||
public static function matches(Route $route, $patterns): bool
|
||||
{
|
||||
$routeName = $route->getName();
|
||||
$routePathWithoutInitialSlash = $route->uri();
|
||||
$routePathWithInitialSlash = "/{$routePathWithoutInitialSlash}";
|
||||
$routeMethods = $route->methods();
|
||||
if (Str::is($patterns, $routeName)
|
||||
|| Str::is($patterns, $routePathWithoutInitialSlash)
|
||||
|| Str::is($patterns, $routePathWithInitialSlash)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($routeMethods as $httpMethod) {
|
||||
if (Str::is($patterns, "{$httpMethod} {$routePathWithoutInitialSlash}")
|
||||
|| Str::is($patterns, "{$httpMethod} {$routePathWithInitialSlash}")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user