update after stop build
All checks were successful
All checks were successful
This commit is contained in:
172
vendor/knuckleswtf/scribe/camel/BaseDTO.php
vendored
Normal file
172
vendor/knuckleswtf/scribe/camel/BaseDTO.php
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
class BaseDTO implements \ArrayAccess, Arrayable
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
* Added so end-users can dynamically add additional properties for their own use
|
||||
*/
|
||||
public array $custom = [];
|
||||
|
||||
public function __construct(array $parameters = [])
|
||||
{
|
||||
// Initialize all properties to their default values first
|
||||
$this->initializeProperties();
|
||||
|
||||
foreach ($parameters as $key => $value) {
|
||||
if (property_exists($this, $key)) {
|
||||
$this->{$key} = $this->castProperty($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function create(array|self $data, array|self $inheritFrom = []): static
|
||||
{
|
||||
if ($data instanceof static) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$mergedData = $inheritFrom instanceof static ? $inheritFrom->toArray() : $inheritFrom;
|
||||
|
||||
foreach ($data as $property => $value) {
|
||||
$mergedData[$property] = $value;
|
||||
}
|
||||
|
||||
return new static($mergedData);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = [];
|
||||
foreach (get_object_vars($this) as $property => $value) {
|
||||
$array[$property] = $value;
|
||||
}
|
||||
|
||||
return $this->parseArray($array);
|
||||
}
|
||||
|
||||
public static function make(array|self $data): static
|
||||
{
|
||||
return $data instanceof static ? $data : new static($data);
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return isset($this->{$offset});
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->{$offset};
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
$this->{$offset} = $value;
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
unset($this->{$offset});
|
||||
}
|
||||
|
||||
public function except(string ...$keys): array
|
||||
{
|
||||
$array = [];
|
||||
foreach (get_object_vars($this) as $property => $value) {
|
||||
if (! in_array($property, $keys)) {
|
||||
$array[$property] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->parseArray($array);
|
||||
}
|
||||
|
||||
public static function arrayOf(array $items): array
|
||||
{
|
||||
return array_map(function ($item) {
|
||||
return $item instanceof static ? $item : new static($item);
|
||||
}, $items);
|
||||
}
|
||||
|
||||
protected function initializeProperties(): void
|
||||
{
|
||||
$reflection = new \ReflectionClass($this);
|
||||
|
||||
foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
|
||||
$name = $property->getName();
|
||||
|
||||
// Skip if already initialized (has a default value)
|
||||
if ($property->hasDefaultValue()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $property->getType();
|
||||
if ($type && $type->allowsNull()) {
|
||||
$this->{$name} = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function castProperty(string $key, mixed $value): mixed
|
||||
{
|
||||
// If the value is already the correct type, return it as-is
|
||||
if (! is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Get property type through reflection
|
||||
$reflection = new \ReflectionClass($this);
|
||||
if (! $reflection->hasProperty($key)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$property = $reflection->getProperty($key);
|
||||
$type = $property->getType();
|
||||
|
||||
if ($type && $type instanceof \ReflectionNamedType && ! $type->isBuiltin()) {
|
||||
$className = $type->getName();
|
||||
|
||||
// If it's a DTO class in our namespace, instantiate it
|
||||
if (class_exists($className) && is_subclass_of($className, self::class)) {
|
||||
return new $className($value);
|
||||
}
|
||||
|
||||
// If it's another class in our namespace that has a constructor accepting arrays
|
||||
if (class_exists($className)) {
|
||||
try {
|
||||
return new $className($value);
|
||||
} catch (\Throwable $e) {
|
||||
// If instantiation fails, return the original value
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function parseArray(array $array): array
|
||||
{
|
||||
// Reimplementing here so our DTOCollection items can be recursively toArray'ed
|
||||
foreach ($array as $key => $value) {
|
||||
if ($value instanceof Arrayable) {
|
||||
$array[$key] = $value->toArray();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! is_array($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$array[$key] = $this->parseArray($value);
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
50
vendor/knuckleswtf/scribe/camel/BaseDTOCollection.php
vendored
Normal file
50
vendor/knuckleswtf/scribe/camel/BaseDTOCollection.php
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @template T of \Knuckles\Camel\BaseDTO
|
||||
*/
|
||||
class BaseDTOCollection extends Collection
|
||||
{
|
||||
/**
|
||||
* @var string the name of the base DTO class
|
||||
*/
|
||||
public static string $base = '';
|
||||
|
||||
public function __construct($items = [])
|
||||
{
|
||||
// Manually cast nested arrays
|
||||
$items = array_map(
|
||||
fn ($item) => is_array($item) ? new static::$base($item) : $item,
|
||||
$items instanceof Collection ? $items->toArray() : $items
|
||||
);
|
||||
|
||||
parent::__construct($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append items to the collection, mutating it.
|
||||
*
|
||||
* @param array[]|T[] $items
|
||||
*/
|
||||
public function concat($items)
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
$this->push(is_array($item) ? new static::$base($item) : $item);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_map(
|
||||
fn ($item) => $item instanceof Arrayable ? $item->toArray() : $item,
|
||||
$this->items
|
||||
);
|
||||
}
|
||||
}
|
||||
231
vendor/knuckleswtf/scribe/camel/Camel.php
vendored
Normal file
231
vendor/knuckleswtf/scribe/camel/Camel.php
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Camel\Output\OutputEndpointData;
|
||||
use Knuckles\Scribe\Tools\PathConfig;
|
||||
use Knuckles\Scribe\Tools\Utils;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Camel
|
||||
{
|
||||
public static function cacheDir(PathConfig $paths): string
|
||||
{
|
||||
return $paths->intermediateOutputPath('endpoints.cache');
|
||||
}
|
||||
|
||||
public static function camelDir(PathConfig $paths): string
|
||||
{
|
||||
return $paths->intermediateOutputPath('endpoints');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load endpoints from the Camel files into groups (arrays).
|
||||
*
|
||||
* @return array[] each array is a group with keys including `name` and `endpoints`
|
||||
*/
|
||||
public static function loadEndpointsIntoGroups(string $folder): array
|
||||
{
|
||||
$groups = [];
|
||||
self::loadEndpointsFromCamelFiles($folder, function (array $group) use (&$groups) {
|
||||
$groups[$group['name']] = $group;
|
||||
});
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load endpoints from the Camel files into a flat list of endpoint arrays.
|
||||
* Useful when we don't care about groups, but simply want to compare endpoints contents
|
||||
* to see if anything changed.
|
||||
*
|
||||
* @return array[] list of endpoint arrays
|
||||
*/
|
||||
public static function loadEndpointsToFlatPrimitivesArray(string $folder): array
|
||||
{
|
||||
$endpoints = [];
|
||||
self::loadEndpointsFromCamelFiles($folder, function (array $group) use (&$endpoints) {
|
||||
foreach ($group['endpoints'] as $endpoint) {
|
||||
$endpoints[] = $endpoint;
|
||||
}
|
||||
});
|
||||
|
||||
return $endpoints;
|
||||
}
|
||||
|
||||
public static function loadEndpointsFromCamelFiles(string $folder, callable $callback): void
|
||||
{
|
||||
$contents = Utils::listDirectoryContents($folder);
|
||||
|
||||
foreach ($contents as $object) {
|
||||
if (
|
||||
$object->isFile()
|
||||
&& Str::endsWith(basename($object->path()), '.yaml')
|
||||
&& ! Str::startsWith(basename($object->path()), 'custom.')
|
||||
) {
|
||||
$group = Yaml::parseFile($object['path']);
|
||||
$callback($group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function loadUserDefinedEndpoints(string $folder): array
|
||||
{
|
||||
$contents = Utils::listDirectoryContents($folder);
|
||||
|
||||
$userDefinedEndpoints = [];
|
||||
foreach ($contents as $object) {
|
||||
if (
|
||||
$object->isFile()
|
||||
&& Str::endsWith(basename($object->path()), '.yaml')
|
||||
&& Str::startsWith(basename($object->path()), 'custom.')
|
||||
) {
|
||||
$endpoints = Yaml::parseFile($object->path());
|
||||
foreach (($endpoints ?: []) as $endpoint) {
|
||||
$userDefinedEndpoints[] = $endpoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $userDefinedEndpoints;
|
||||
}
|
||||
|
||||
public static function doesGroupContainEndpoint(array $group, OutputEndpointData $endpoint): bool
|
||||
{
|
||||
return (bool) (Arr::first($group['endpoints'], function ($e) use ($endpoint) {
|
||||
return $e->endpointId() === $endpoint->endpointId();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array[] $groupedEndpoints
|
||||
* @param array $configFileOrder the order for groups that users specified in their config file
|
||||
* @return array[]
|
||||
*/
|
||||
public static function sortByConfigFileOrder(array $groupedEndpoints, array $configFileOrder): array
|
||||
{
|
||||
if (empty($configFileOrder)) {
|
||||
ksort($groupedEndpoints, SORT_NATURAL);
|
||||
|
||||
return $groupedEndpoints;
|
||||
}
|
||||
|
||||
// First, sort groups
|
||||
$groupsOrder = Utils::getTopLevelItemsFromMixedConfigList($configFileOrder);
|
||||
$groupsCollection = collect($groupedEndpoints);
|
||||
$wildcardPosition = array_search('*', $groupsOrder);
|
||||
if ($wildcardPosition !== false) {
|
||||
$promotedGroups = array_splice($groupsOrder, 0, $wildcardPosition);
|
||||
$demotedGroups = array_splice($groupsOrder, 1);
|
||||
|
||||
$promotedOrderedGroups = $groupsCollection->filter(fn ($group, $groupName) => in_array($groupName, $promotedGroups))
|
||||
->sortKeysUsing(self::getOrderListComparator($promotedGroups));
|
||||
$demotedOrderedGroups = $groupsCollection->filter(fn ($group, $groupName) => in_array($groupName, $demotedGroups))
|
||||
->sortKeysUsing(self::getOrderListComparator($demotedGroups));
|
||||
|
||||
$nonWildcardGroups = array_merge($promotedGroups, $demotedGroups);
|
||||
$wildCardOrderedGroups = $groupsCollection->filter(fn ($group, $groupName) => ! in_array($groupName, $nonWildcardGroups))
|
||||
->sortKeysUsing(self::getOrderListComparator($demotedGroups));
|
||||
|
||||
$groupedEndpoints = $promotedOrderedGroups->merge($wildCardOrderedGroups)
|
||||
->merge($demotedOrderedGroups);
|
||||
} else {
|
||||
$groupedEndpoints = $groupsCollection->sortKeysUsing(self::getOrderListComparator($groupsOrder));
|
||||
}
|
||||
|
||||
return $groupedEndpoints->map(function (array $group, string $groupName) use ($configFileOrder) {
|
||||
$sortedEndpoints = collect($group['endpoints']);
|
||||
|
||||
if (isset($configFileOrder[$groupName])) {
|
||||
// Second-level order list. Can contain endpoint or subgroup names
|
||||
$level2Order = Utils::getTopLevelItemsFromMixedConfigList($configFileOrder[$groupName]);
|
||||
$sortedEndpoints = $sortedEndpoints->sortBy(
|
||||
function (OutputEndpointData $e) use ($configFileOrder, $level2Order) {
|
||||
$endpointIdentifier = $e->httpMethods[0].' /'.$e->uri;
|
||||
|
||||
// First, check if there's an ordering specified for the endpoint itself
|
||||
$indexOfEndpointInL2Order = array_search($endpointIdentifier, $level2Order);
|
||||
if ($indexOfEndpointInL2Order !== false) {
|
||||
return $indexOfEndpointInL2Order;
|
||||
}
|
||||
|
||||
// Check if there's an ordering for the endpoint's subgroup
|
||||
$indexOfSubgroupInL2Order = array_search($e->metadata->subgroup, $level2Order);
|
||||
if ($indexOfSubgroupInL2Order !== false) {
|
||||
// There's a subgroup order; check if there's an endpoints order within that
|
||||
$orderOfEndpointsInSubgroup = $configFileOrder[$e->metadata->groupName][$e->metadata->subgroup] ?? [];
|
||||
$indexOfEndpointInSubGroup = array_search($endpointIdentifier, $orderOfEndpointsInSubgroup);
|
||||
|
||||
return ($indexOfEndpointInSubGroup === false)
|
||||
? $indexOfSubgroupInL2Order
|
||||
: ($indexOfSubgroupInL2Order + ($indexOfEndpointInSubGroup * 0.1));
|
||||
}
|
||||
|
||||
return INF;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $groupName,
|
||||
'description' => $group['description'],
|
||||
'endpoints' => $sortedEndpoints->all(),
|
||||
];
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare endpoints to be turned into HTML.
|
||||
* Map them into OutputEndpointData DTOs, and sort them by the specified order in the config file.
|
||||
*
|
||||
* @param array<string,array[]> $groupedEndpoints
|
||||
*/
|
||||
public static function prepareGroupedEndpointsForOutput(array $groupedEndpoints, array $configFileOrder = []): array
|
||||
{
|
||||
$groups = array_map(function (array $group) {
|
||||
return [
|
||||
'name' => $group['name'],
|
||||
'description' => $group['description'],
|
||||
'endpoints' => array_map(
|
||||
fn (array $endpoint) => OutputEndpointData::fromExtractedEndpointArray($endpoint),
|
||||
$group['endpoints']
|
||||
),
|
||||
];
|
||||
}, $groupedEndpoints);
|
||||
|
||||
return self::sortByConfigFileOrder($groups, $configFileOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an $order list like ['first', 'second', ...], return a compare function that can be used to sort
|
||||
* a list of strings based on the order of items in $order.
|
||||
* Any strings not in the list are sorted with natural sort.
|
||||
*/
|
||||
protected static function getOrderListComparator(array $order): \Closure
|
||||
{
|
||||
return function ($a, $b) use ($order) {
|
||||
$indexOfA = array_search($a, $order);
|
||||
$indexOfB = array_search($b, $order);
|
||||
|
||||
// If both are in the $order list, compare them normally based on their position in the list
|
||||
if ($indexOfA !== false && $indexOfB !== false) {
|
||||
return $indexOfA <=> $indexOfB;
|
||||
}
|
||||
|
||||
// If only A is in the $order list, then it must come before B.
|
||||
if ($indexOfA !== false) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If only B is in the $order list, then it must come before A.
|
||||
if ($indexOfB !== false) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If neither is present, fall back to natural sort
|
||||
return strnatcmp($a, $b);
|
||||
};
|
||||
}
|
||||
}
|
||||
169
vendor/knuckleswtf/scribe/camel/Extraction/ExtractedEndpointData.php
vendored
Normal file
169
vendor/knuckleswtf/scribe/camel/Extraction/ExtractedEndpointData.php
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel\Extraction;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Route;
|
||||
use Knuckles\Camel\BaseDTO;
|
||||
use Knuckles\Scribe\Extracting\Shared\UrlParamsNormalizer;
|
||||
use Knuckles\Scribe\Tools\Globals;
|
||||
use Knuckles\Scribe\Tools\Utils as u;
|
||||
|
||||
class ExtractedEndpointData extends BaseDTO
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public array $httpMethods;
|
||||
|
||||
public string $uri;
|
||||
|
||||
public Metadata $metadata;
|
||||
|
||||
/**
|
||||
* @var array<string,string>
|
||||
*/
|
||||
public array $headers = [];
|
||||
|
||||
/**
|
||||
* @var array<string,Parameter>
|
||||
*/
|
||||
public array $urlParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
public array $cleanUrlParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,Parameter>
|
||||
*/
|
||||
public array $queryParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
public array $cleanQueryParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,Parameter>
|
||||
*/
|
||||
public array $bodyParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
public array $cleanBodyParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,array|UploadedFile>
|
||||
*/
|
||||
public array $fileParameters = [];
|
||||
|
||||
public ResponseCollection $responses;
|
||||
|
||||
/**
|
||||
* @var array<string,ResponseField>
|
||||
*/
|
||||
public array $responseFields = [];
|
||||
|
||||
/**
|
||||
* Authentication info for this endpoint. In the form [{where}, {name}, {sample}]
|
||||
* Example: ["queryParameters", "api_key", "njiuyiw97865rfyvgfvb1"].
|
||||
*/
|
||||
public array $auth = [];
|
||||
|
||||
public ?\ReflectionClass $controller;
|
||||
|
||||
public ?\ReflectionFunctionAbstract $method;
|
||||
|
||||
public ?Route $route;
|
||||
|
||||
public function __construct(array $parameters = [])
|
||||
{
|
||||
$parameters['metadata'] ??= new Metadata([]);
|
||||
$parameters['responses'] ??= new ResponseCollection([]);
|
||||
|
||||
parent::__construct($parameters);
|
||||
|
||||
$defaultNormalizer = fn () => UrlParamsNormalizer::normalizeParameterNamesInRouteUri($this->route, $this->method);
|
||||
$this->uri = match (is_callable(Globals::$__normalizeEndpointUrlUsing)) {
|
||||
true => call_user_func_array(
|
||||
Globals::$__normalizeEndpointUrlUsing,
|
||||
[$this->route->uri, $this->route, $this->method, $this->controller, $defaultNormalizer]
|
||||
),
|
||||
default => $defaultNormalizer(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $extras Only used for quick overrides in tests
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public static function fromRoute(Route $route, array $extras = []): self
|
||||
{
|
||||
$httpMethods = self::getMethods($route);
|
||||
$uri = $route->uri();
|
||||
|
||||
[$controllerName, $methodName] = u::getRouteClassAndMethodNames($route);
|
||||
$controller = new \ReflectionClass($controllerName);
|
||||
$method = u::getReflectedRouteMethod([$controllerName, $methodName]);
|
||||
|
||||
$data = compact('httpMethods', 'uri', 'controller', 'method', 'route');
|
||||
$data = array_merge($data, $extras);
|
||||
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function getMethods(Route $route): array
|
||||
{
|
||||
$methods = $route->methods();
|
||||
|
||||
// Laravel adds an automatic "HEAD" endpoint for each GET request, so we'll strip that out,
|
||||
// but not if there's only one method (means it was intentional)
|
||||
if (count($methods) === 1) {
|
||||
return $methods;
|
||||
}
|
||||
|
||||
return array_diff($methods, ['HEAD']);
|
||||
}
|
||||
|
||||
public function name()
|
||||
{
|
||||
return sprintf("[%s] {$this->route->uri}.", implode(',', $this->route->methods));
|
||||
}
|
||||
|
||||
public function endpointId()
|
||||
{
|
||||
return $this->httpMethods[0].str_replace(['/', '?', '{', '}', ':', '\\', '+', '|'], '-', $this->uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the endpoint data for serialising.
|
||||
*/
|
||||
public function forSerialisation()
|
||||
{
|
||||
$copyArray = $this->except(
|
||||
// Get rid of all duplicate data
|
||||
'cleanQueryParameters',
|
||||
'cleanUrlParameters',
|
||||
'fileParameters',
|
||||
'cleanBodyParameters',
|
||||
// and objects used only in extraction
|
||||
'route',
|
||||
'controller',
|
||||
'method',
|
||||
'auth',
|
||||
);
|
||||
// Remove these, since they're on the parent group object
|
||||
if (isset($copyArray['metadata']) && $copyArray['metadata'] instanceof Metadata) {
|
||||
$copyArray['metadata'] = $copyArray['metadata']->except('groupName', 'groupDescription');
|
||||
}
|
||||
|
||||
return $copyArray;
|
||||
}
|
||||
}
|
||||
24
vendor/knuckleswtf/scribe/camel/Extraction/Metadata.php
vendored
Normal file
24
vendor/knuckleswtf/scribe/camel/Extraction/Metadata.php
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel\Extraction;
|
||||
|
||||
use Knuckles\Camel\BaseDTO;
|
||||
|
||||
class Metadata extends BaseDTO
|
||||
{
|
||||
public ?string $groupName;
|
||||
|
||||
public ?string $groupDescription;
|
||||
|
||||
public ?string $subgroup;
|
||||
|
||||
public ?string $subgroupDescription;
|
||||
|
||||
public ?string $title;
|
||||
|
||||
public ?string $description;
|
||||
|
||||
public bool $authenticated = false;
|
||||
|
||||
public bool|string $deprecated = false;
|
||||
}
|
||||
32
vendor/knuckleswtf/scribe/camel/Extraction/Parameter.php
vendored
Normal file
32
vendor/knuckleswtf/scribe/camel/Extraction/Parameter.php
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel\Extraction;
|
||||
|
||||
use Knuckles\Camel\BaseDTO;
|
||||
|
||||
class Parameter extends BaseDTO
|
||||
{
|
||||
public string $name;
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
public bool $required = false;
|
||||
|
||||
public mixed $example = null;
|
||||
|
||||
public string $type = 'string';
|
||||
|
||||
public array $enumValues = [];
|
||||
|
||||
public bool $exampleWasSpecified = false;
|
||||
|
||||
public bool $nullable = false;
|
||||
|
||||
public bool $deprecated = false;
|
||||
|
||||
public function __construct(array $parameters = [])
|
||||
{
|
||||
unset($parameters['setter']);
|
||||
parent::__construct($parameters);
|
||||
}
|
||||
}
|
||||
67
vendor/knuckleswtf/scribe/camel/Extraction/Response.php
vendored
Normal file
67
vendor/knuckleswtf/scribe/camel/Extraction/Response.php
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel\Extraction;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Camel\BaseDTO;
|
||||
|
||||
class Response extends BaseDTO
|
||||
{
|
||||
public int $status;
|
||||
|
||||
public ?string $content;
|
||||
|
||||
public array $headers = [];
|
||||
|
||||
public ?string $description;
|
||||
|
||||
public function __construct(array $parameters = [])
|
||||
{
|
||||
if (is_array($parameters['content'] ?? null)) {
|
||||
$parameters['content'] = json_encode($parameters['content'], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
if (isset($parameters['status'])) {
|
||||
$parameters['status'] = (int) $parameters['status'];
|
||||
}
|
||||
|
||||
$hiddenHeaders = [
|
||||
'date',
|
||||
'Date',
|
||||
'etag',
|
||||
'ETag',
|
||||
'last-modified',
|
||||
'Last-Modified',
|
||||
'date',
|
||||
'Date',
|
||||
'content-length',
|
||||
'Content-Length',
|
||||
'connection',
|
||||
'Connection',
|
||||
'x-powered-by',
|
||||
'X-Powered-By',
|
||||
];
|
||||
if (! empty($parameters['headers'])) {
|
||||
foreach ($hiddenHeaders as $headerName) {
|
||||
unset($parameters['headers'][$headerName]);
|
||||
}
|
||||
}
|
||||
|
||||
parent::__construct($parameters);
|
||||
}
|
||||
|
||||
public function fullDescription()
|
||||
{
|
||||
$description = $this->status;
|
||||
if ($this->description) {
|
||||
$description .= ", {$this->description}";
|
||||
}
|
||||
|
||||
return $description;
|
||||
}
|
||||
|
||||
public function isBinary(): bool
|
||||
{
|
||||
return is_string($this->content) && Str::startsWith($this->content, '<<binary>>');
|
||||
}
|
||||
}
|
||||
20
vendor/knuckleswtf/scribe/camel/Extraction/ResponseCollection.php
vendored
Normal file
20
vendor/knuckleswtf/scribe/camel/Extraction/ResponseCollection.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel\Extraction;
|
||||
|
||||
use Knuckles\Camel\BaseDTOCollection;
|
||||
|
||||
/**
|
||||
* @extends BaseDTOCollection<Response>
|
||||
*/
|
||||
class ResponseCollection extends BaseDTOCollection
|
||||
{
|
||||
public static string $base = Response::class;
|
||||
|
||||
public function hasSuccessResponse(): bool
|
||||
{
|
||||
return $this->first(
|
||||
fn ($response) => '2' === ((string) ($response->status))[0]
|
||||
) !== null;
|
||||
}
|
||||
}
|
||||
31
vendor/knuckleswtf/scribe/camel/Extraction/ResponseField.php
vendored
Normal file
31
vendor/knuckleswtf/scribe/camel/Extraction/ResponseField.php
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel\Extraction;
|
||||
|
||||
use Knuckles\Camel\BaseDTO;
|
||||
|
||||
class ResponseField extends BaseDTO
|
||||
{
|
||||
// TODO make this extend Parameter, so we can have strong types and a unified API
|
||||
// but first we need to normalize incoming data
|
||||
|
||||
/** @var string */
|
||||
public $name;
|
||||
|
||||
/** @var string */
|
||||
public $description;
|
||||
|
||||
/** @var string */
|
||||
public $type;
|
||||
|
||||
/** @var bool */
|
||||
public $required;
|
||||
|
||||
/** @var mixed */
|
||||
public $example;
|
||||
|
||||
public array $enumValues = [];
|
||||
|
||||
/** @var bool */
|
||||
public $nullable;
|
||||
}
|
||||
355
vendor/knuckleswtf/scribe/camel/Output/OutputEndpointData.php
vendored
Normal file
355
vendor/knuckleswtf/scribe/camel/Output/OutputEndpointData.php
vendored
Normal file
@@ -0,0 +1,355 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel\Output;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Knuckles\Camel\BaseDTO;
|
||||
use Knuckles\Camel\Extraction\Metadata;
|
||||
use Knuckles\Camel\Extraction\ResponseCollection;
|
||||
use Knuckles\Camel\Extraction\ResponseField;
|
||||
use Knuckles\Scribe\Extracting\Extractor;
|
||||
use Knuckles\Scribe\Tools\Utils as u;
|
||||
use Knuckles\Scribe\Tools\WritingUtils;
|
||||
|
||||
/**
|
||||
* Endpoint DTO, optimized for generating HTML output.
|
||||
* Unneeded properties removed, extra properties and helper methods added.
|
||||
*/
|
||||
class OutputEndpointData extends BaseDTO
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public array $httpMethods;
|
||||
|
||||
public string $uri;
|
||||
|
||||
public Metadata $metadata;
|
||||
|
||||
/**
|
||||
* @var array<string,string>
|
||||
*/
|
||||
public array $headers = [];
|
||||
|
||||
/**
|
||||
* @var array<string,Parameter>
|
||||
*/
|
||||
public array $urlParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
public array $cleanUrlParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,Parameter>
|
||||
*/
|
||||
public array $queryParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
public array $cleanQueryParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string, Parameter>
|
||||
*/
|
||||
public array $bodyParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
public array $cleanBodyParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string,UploadedFile>
|
||||
*/
|
||||
public array $fileParameters = [];
|
||||
|
||||
public ResponseCollection $responses;
|
||||
|
||||
/**
|
||||
* @var array<string,ResponseField>
|
||||
*/
|
||||
public array $responseFields = [];
|
||||
|
||||
/**
|
||||
* The same as bodyParameters, but organized in a hierarchy.
|
||||
* So, top-level items first, with a __fields property containing their children, and so on.
|
||||
* Useful so we can easily render and nest on the frontend.
|
||||
*
|
||||
* @var array<string, array>
|
||||
*/
|
||||
public array $nestedBodyParameters = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array>
|
||||
*/
|
||||
public array $nestedResponseFields = [];
|
||||
|
||||
public ?string $boundUri;
|
||||
|
||||
public function __construct(array $parameters = [])
|
||||
{
|
||||
// spatie/dto currently doesn't auto-cast nested DTOs like that
|
||||
$parameters['responses'] = new ResponseCollection($parameters['responses'] ?? []);
|
||||
$parameters['bodyParameters'] = array_map(fn ($param) => new Parameter($param), $parameters['bodyParameters'] ?? []);
|
||||
$parameters['queryParameters'] = array_map(fn ($param) => new Parameter($param), $parameters['queryParameters'] ?? []);
|
||||
$parameters['urlParameters'] = array_map(fn ($param) => new Parameter($param), $parameters['urlParameters'] ?? []);
|
||||
$parameters['responseFields'] = array_map(fn ($param) => new ResponseField($param), $parameters['responseFields'] ?? []);
|
||||
|
||||
parent::__construct($parameters);
|
||||
|
||||
$this->cleanBodyParameters = Extractor::cleanParams($this->bodyParameters);
|
||||
$this->cleanQueryParameters = Extractor::cleanParams($this->queryParameters);
|
||||
$this->cleanUrlParameters = Extractor::cleanParams($this->urlParameters);
|
||||
$this->nestedBodyParameters = self::nestArrayAndObjectFields($this->bodyParameters, $this->cleanBodyParameters);
|
||||
$this->nestedResponseFields = self::nestArrayAndObjectFields($this->responseFields);
|
||||
|
||||
$this->boundUri = u::getUrlWithBoundParameters($this->uri, $this->cleanUrlParameters);
|
||||
|
||||
[$files, $regularParameters] = static::splitIntoFileAndRegularParameters($this->cleanBodyParameters);
|
||||
|
||||
if (count($files)) {
|
||||
$this->headers['Content-Type'] = 'multipart/form-data';
|
||||
}
|
||||
$this->fileParameters = $files;
|
||||
$this->cleanBodyParameters = $regularParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function getMethods(Route $route): array
|
||||
{
|
||||
$methods = $route->methods();
|
||||
|
||||
// Laravel adds an automatic "HEAD" endpoint for each GET request, so we'll strip that out,
|
||||
// but not if there's only one method (means it was intentional)
|
||||
if (count($methods) === 1) {
|
||||
return $methods;
|
||||
}
|
||||
|
||||
return array_diff($methods, ['HEAD']);
|
||||
}
|
||||
|
||||
public static function fromExtractedEndpointArray(array $endpoint): self
|
||||
{
|
||||
return new self($endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform body parameters such that object fields have a `fields` property containing a list of all subfields
|
||||
* Subfields will be removed from the main parameter map
|
||||
* For instance, if $parameters is [
|
||||
* 'dad' => new Parameter(...),
|
||||
* 'dad.age' => new Parameter(...),
|
||||
* 'dad.cars[]' => new Parameter(...),
|
||||
* 'dad.cars[].model' => new Parameter(...),
|
||||
* 'dad.cars[].price' => new Parameter(...),
|
||||
* ],
|
||||
* normalise this into [
|
||||
* 'dad' => [
|
||||
* ...,
|
||||
* '__fields' => [
|
||||
* 'dad.age' => [...],
|
||||
* 'dad.cars' => [
|
||||
* ...,
|
||||
* '__fields' => [
|
||||
* 'model' => [...],
|
||||
* 'price' => [...],
|
||||
* ],
|
||||
* ],
|
||||
* ],
|
||||
* ]].
|
||||
*/
|
||||
public static function nestArrayAndObjectFields(array $parameters, array $cleanParameters = []): array
|
||||
{
|
||||
// First, we'll make sure all object fields have parent fields properly set
|
||||
$normalisedParameters = [];
|
||||
foreach ($parameters as $name => $parameter) {
|
||||
if (Str::contains($name, '.')) {
|
||||
// If the user didn't add a parent field, we'll helpfully add it for them
|
||||
$ancestors = [];
|
||||
|
||||
$parts = explode('.', $name);
|
||||
$fieldName = array_pop($parts);
|
||||
$parentName = mb_rtrim(implode('.', $parts), '[]');
|
||||
|
||||
// When the body is an array, param names will be "[].paramname",
|
||||
// so $parentName is empty. Let's fix that.
|
||||
if (empty($parentName)) {
|
||||
$parentName = '[]';
|
||||
}
|
||||
|
||||
while ($parentName) {
|
||||
if (! empty($normalisedParameters[$parentName])) {
|
||||
break;
|
||||
}
|
||||
|
||||
$details = [
|
||||
'name' => $parentName,
|
||||
'type' => $parentName === '[]' ? 'object[]' : 'object',
|
||||
'description' => '',
|
||||
'required' => false,
|
||||
];
|
||||
|
||||
if ($parameter instanceof ResponseField) {
|
||||
$ancestors[] = [$parentName, new ResponseField($details)];
|
||||
} else {
|
||||
$lastParentExample = $details['example']
|
||||
= [$fieldName => $lastParentExample ?? $parameter->example];
|
||||
$ancestors[] = [$parentName, new Parameter($details)];
|
||||
}
|
||||
|
||||
$fieldName = array_pop($parts);
|
||||
$parentName = mb_rtrim(implode('.', $parts), '[]');
|
||||
}
|
||||
|
||||
// We add ancestors in reverse so we can iterate over parents first in the next section
|
||||
foreach (array_reverse($ancestors) as [$ancestorName, $ancestor]) {
|
||||
$normalisedParameters[$ancestorName] = $ancestor;
|
||||
}
|
||||
}
|
||||
|
||||
$normalisedParameters[$name] = $parameter;
|
||||
unset($lastParentExample);
|
||||
}
|
||||
|
||||
$finalParameters = [];
|
||||
foreach ($normalisedParameters as $name => $parameter) {
|
||||
$parameter = $parameter->toArray();
|
||||
if (Str::contains($name, '.')) { // An object field
|
||||
// Get the various pieces of the name
|
||||
$parts = explode('.', $name);
|
||||
$fieldName = array_pop($parts);
|
||||
$baseName = implode('.__fields.', $parts);
|
||||
|
||||
// For subfields, the type is indicated in the source object
|
||||
// eg test.items[].more and test.items.more would both have parent field with name `items` and containing __fields => more
|
||||
// The difference would be in the parent field's `type` property (object[] vs object)
|
||||
// So we can get rid of all [] to get the parent name
|
||||
$dotPathToParent = str_replace('[]', '', $baseName);
|
||||
// When the body is an array, param names will be "[].paramname",
|
||||
// so $parts is ['[]']
|
||||
if ($parts[0] === '[]') {
|
||||
$dotPathToParent = '[]'.$dotPathToParent;
|
||||
}
|
||||
|
||||
$dotPath = $dotPathToParent.'.__fields.'.$fieldName;
|
||||
Arr::set($finalParameters, $dotPath, $parameter);
|
||||
} else { // A regular field, not a subfield of anything
|
||||
// Note: we're assuming any subfields of this field are listed *after* it,
|
||||
// and will set __fields correctly when we iterate over them
|
||||
// Hence why we create a new "normalisedParameters" array above and push the parent to that first
|
||||
$parameter['__fields'] = [];
|
||||
$finalParameters[$name] = $parameter;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, if the body is an array, remove any other items.
|
||||
if (isset($finalParameters['[]'])) {
|
||||
$finalParameters = ['[]' => $finalParameters['[]']];
|
||||
// At this point, the examples are likely [[], []],
|
||||
// but have been correctly set in clean parameters, so let's update them
|
||||
if ($finalParameters['[]']['example'][0] === [] && ! empty($cleanParameters)) {
|
||||
$finalParameters['[]']['example'] = $cleanParameters;
|
||||
}
|
||||
}
|
||||
|
||||
return $finalParameters;
|
||||
}
|
||||
|
||||
public function endpointId(): string
|
||||
{
|
||||
return $this->httpMethods[0].str_replace(['/', '?', '{', '}', ':', '\\', '+', '|', '.'], '-', $this->uri);
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->metadata->title ?: ($this->httpMethods[0].' '.$this->uri);
|
||||
}
|
||||
|
||||
public function fullSlug(): string
|
||||
{
|
||||
$groupSlug = Str::slug($this->metadata->groupName);
|
||||
$endpointId = $this->endpointId();
|
||||
|
||||
return "{$groupSlug}-{$endpointId}";
|
||||
}
|
||||
|
||||
public function hasResponses(): bool
|
||||
{
|
||||
return count($this->responses) > 0;
|
||||
}
|
||||
|
||||
public function hasFiles(): bool
|
||||
{
|
||||
return count($this->fileParameters) > 0;
|
||||
}
|
||||
|
||||
public function isArrayBody(): bool
|
||||
{
|
||||
return count($this->nestedBodyParameters) === 1
|
||||
&& array_keys($this->nestedBodyParameters)[0] === '[]';
|
||||
}
|
||||
|
||||
public function isGet(): bool
|
||||
{
|
||||
return in_array('GET', $this->httpMethods);
|
||||
}
|
||||
|
||||
public function isAuthed(): bool
|
||||
{
|
||||
return $this->metadata->authenticated;
|
||||
}
|
||||
|
||||
public function hasJsonBody(): bool
|
||||
{
|
||||
if ($this->hasFiles() || empty($this->nestedBodyParameters)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$contentType = data_get($this->headers, 'Content-Type', data_get($this->headers, 'content-type', ''));
|
||||
|
||||
return str_contains($contentType, 'json');
|
||||
}
|
||||
|
||||
public function getSampleBody()
|
||||
{
|
||||
return WritingUtils::getSampleBody($this->nestedBodyParameters);
|
||||
}
|
||||
|
||||
public function hasHeadersOrQueryOrBodyParams(): bool
|
||||
{
|
||||
return ! empty($this->headers)
|
||||
|| ! empty($this->cleanQueryParameters)
|
||||
|| ! empty($this->cleanBodyParameters);
|
||||
}
|
||||
|
||||
public static function splitIntoFileAndRegularParameters(array $parameters): array
|
||||
{
|
||||
$files = [];
|
||||
$regularParameters = [];
|
||||
foreach ($parameters as $name => $example) {
|
||||
if ($example instanceof UploadedFile) {
|
||||
$files[$name] = $example;
|
||||
} elseif (is_array($example) && ! empty($example)) {
|
||||
[$subFiles, $subRegulars] = static::splitIntoFileAndRegularParameters($example);
|
||||
foreach ($subFiles as $subName => $subExample) {
|
||||
$files[$name][$subName] = $subExample;
|
||||
}
|
||||
foreach ($subRegulars as $subName => $subExample) {
|
||||
$regularParameters[$name][$subName] = $subExample;
|
||||
}
|
||||
} else {
|
||||
$regularParameters[$name] = $example;
|
||||
}
|
||||
}
|
||||
|
||||
return [$files, $regularParameters];
|
||||
}
|
||||
}
|
||||
13
vendor/knuckleswtf/scribe/camel/Output/Parameter.php
vendored
Normal file
13
vendor/knuckleswtf/scribe/camel/Output/Parameter.php
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Knuckles\Camel\Output;
|
||||
|
||||
class Parameter extends \Knuckles\Camel\Extraction\Parameter
|
||||
{
|
||||
public array $__fields = [];
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->except('__fields');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user