Scaffold basic attribute support + refactor tests

This commit is contained in:
shalvah
2022-07-16 00:16:05 +02:00
parent d557ba7bba
commit e4a39367ae
24 changed files with 676 additions and 111 deletions

View File

@@ -8,6 +8,12 @@ use Spatie\DataTransferObject\DataTransferObject;
class BaseDTO extends DataTransferObject implements Arrayable
{
/**
* @var array $custom
* Added so end-users can dynamically add additional properties for their own use.
*/
public array $custom = [];
/**
* @param array|self $data
*
@@ -41,4 +47,4 @@ class BaseDTO extends DataTransferObject implements Arrayable
return $array;
}
}
}

View File

@@ -29,6 +29,4 @@ class Metadata extends BaseDTO
public ?string $description;
public bool $authenticated = false;
public array $custom = [];
}

View File

@@ -10,9 +10,8 @@ class Parameter extends BaseDTO
public string $name;
public ?string $description = null;
public bool $required = false;
public $example = null;
public mixed $example = null;
public string $type = 'string';
public array $custom = [];
public function __construct(array $parameters = [])
{

View File

@@ -5,20 +5,8 @@ namespace Knuckles\Camel\Output;
class Parameter extends \Knuckles\Camel\Extraction\Parameter
{
public string $name;
public ?string $description = null;
public bool $required = false;
public $example = null;
public string $type = 'string';
public array $__fields = [];
public array $custom = [];
public function toArray(): array
{
if (empty($this->exceptKeys)) {

View File

@@ -16,7 +16,7 @@
}
],
"require": {
"php": ">=7.4",
"php": ">=8.0",
"ext-fileinfo": "*",
"ext-json": "*",
"ext-pdo": "*",

View File

@@ -0,0 +1,10 @@
<?php
namespace Knuckles\Scribe\Attributes;
use Attribute;
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class BodyParam extends GenericParam
{
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Knuckles\Scribe\Attributes;
use Attribute;
#[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 function toArray()
{
return [
"name" => $this->name,
"description" => $this->description,
"type" => $this->type,
"required" => $this->required,
"example" => $this->example,
];
}
}

15
src/Attributes/Group.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace Knuckles\Scribe\Attributes;
use Attribute;
#[Attribute]
class Group
{
public function __construct(
public string $name,
public ?string $description,
){
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Knuckles\Scribe\Attributes;
use Attribute;
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class QueryParam extends GenericParam
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Knuckles\Scribe\Attributes;
use Attribute;
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class UrlParam extends GenericParam
{
}

View File

@@ -15,12 +15,7 @@ trait FindsFormRequestForMethod
{
foreach ($method->getParameters() as $argument) {
$argType = $argument->getType();
if ($argType === null) {
continue;
}
if (class_exists(ReflectionUnionType::class)
&& $argType instanceof ReflectionUnionType) {
if ($argType === null || $argType instanceof ReflectionUnionType) {
continue;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Knuckles\Scribe\Extracting\Strategies\BodyParameters;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Extracting\Strategies\GetParamsFromAttributeStrategy;
class GetFromBodyParamAttribute extends GetParamsFromAttributeStrategy
{
protected string $attributeName = BodyParam::class;
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Knuckles\Scribe\Extracting\Strategies;
use Knuckles\Scribe\Extracting\ParamHelpers;
class GetParamsFromAttributeStrategy extends PhpAttributeStrategy
{
use ParamHelpers;
protected function extractFromAttributes(array $attributesOnMethod, array $attributesOnController): ?array
{
$parameters = [];
foreach ($attributesOnController as $attributeInstance) {
$parameters[$attributeInstance->name] = $attributeInstance->toArray();
}
foreach ($attributesOnMethod as $attributeInstance) {
$parameters[$attributeInstance->name] = $attributeInstance->toArray();
}
return array_map([$this, 'normalizeParameterData'], $parameters);
}
protected function normalizeParameterData(array $data): array
{
$data['type'] = $this->normalizeTypeName($data['type']);
if (is_null($data['example'])) {
$data['example'] = $this->generateDummyValue($data['type']);
} else if ($data['example'] == 'No-example' || $data['example'] == 'No-example.') {
$data['example'] = null;
}
$data['description'] = trim($data['description'] ?? '');
return $data;
}
}

View File

@@ -22,12 +22,7 @@ class GetFromHeaderTag extends Strategy
{
foreach ($endpointData->method->getParameters() as $param) {
$paramType = $param->getType();
if ($paramType === null) {
continue;
}
if (class_exists(ReflectionUnionType::class)
&& $paramType instanceof ReflectionUnionType) {
if ($paramType === null || $paramType instanceof ReflectionUnionType) {
continue;
}

View File

@@ -20,14 +20,14 @@ class GetFromDocBlocks extends Strategy
public function getMetadataFromDocBlock(DocBlock $methodDocBlock, DocBlock $classDocBlock): array
{
[$routeGroupName, $routeGroupDescription, $routeTitle] = $this->getEndpointGroupDetails($methodDocBlock, $classDocBlock);
[$groupName, $groupDescription, $title] = $this->getEndpointGroupAndTitleDetails($methodDocBlock, $classDocBlock);
return [
'groupName' => $routeGroupName,
'groupDescription' => $routeGroupDescription,
'groupName' => $groupName,
'groupDescription' => $groupDescription,
'subgroup' => $this->getEndpointSubGroup($methodDocBlock, $classDocBlock),
'subgroupDescription' => $this->getEndpointSubGroupDescription($methodDocBlock, $classDocBlock),
'title' => $routeTitle ?: $methodDocBlock->getShortDescription(),
'title' => $title ?: $methodDocBlock->getShortDescription(),
'description' => $methodDocBlock->getLongDescription()->getContents(),
'authenticated' => $this->getAuthStatusFromDocBlock($methodDocBlock, $classDocBlock),
];
@@ -51,9 +51,9 @@ class GetFromDocBlocks extends Strategy
}
/**
* @return array The route group name, the group description, and the route title
* @return array The endpoint's group name, the group description, and the endpoint title
*/
protected function getEndpointGroupDetails(DocBlock $methodDocBlock, DocBlock $controllerDocBlock)
protected function getEndpointGroupAndTitleDetails(DocBlock $methodDocBlock, DocBlock $controllerDocBlock)
{
foreach ($methodDocBlock->getTags() as $tag) {
if ($tag->getName() === 'group') {

View File

@@ -0,0 +1,57 @@
<?php
namespace Knuckles\Scribe\Extracting\Strategies;
use Knuckles\Camel\Extraction\ExtractedEndpointData;
use Knuckles\Scribe\Extracting\ParamHelpers;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionFunctionAbstract;
/**
* @template T of \ReflectionAttribute
*/
abstract class PhpAttributeStrategy extends Strategy
{
use ParamHelpers;
/**
* @var class-string<T>
*/
protected string $attributeName;
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules): array
{
[$attributesOnMethod, $attributesOnController] =
$this->getAttributes($endpointData->method, $endpointData->controller);
return $this->extractFromAttributes($attributesOnMethod, $attributesOnController);
}
/**
* @param \ReflectionFunctionAbstract $method
* @param \ReflectionClass|null $class
*
* @return array{array<T>, array<T>}
*/
protected function getAttributes(ReflectionFunctionAbstract $method, ?ReflectionClass $class = null): array
{
$attributesOnMethod = array_map(
fn(ReflectionAttribute $a) => $a->newInstance(), $method->getAttributes($this->attributeName)
);
if ($class) {
$attributesOnController = array_map(
fn(ReflectionAttribute $a) => $a->newInstance(), $class->getAttributes($this->attributeName)
);
}
return [$attributesOnMethod, $attributesOnController ?? []];
}
/**
* @param array<T> $attributesOnMethod
* @param array<T> $attributesOnController
*/
abstract protected function extractFromAttributes(array $attributesOnMethod, array $attributesOnController): ?array;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Knuckles\Scribe\Extracting\Strategies\QueryParameters;
use Knuckles\Scribe\Attributes\QueryParam;
use Knuckles\Scribe\Extracting\Strategies\GetParamsFromAttributeStrategy;
class GetFromQueryParamAttribute extends GetParamsFromAttributeStrategy
{
protected string $attributeName = QueryParam::class;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Knuckles\Scribe\Extracting\Strategies\UrlParameters;
use Knuckles\Scribe\Attributes\UrlParam;
use Knuckles\Scribe\Extracting\Strategies\GetParamsFromAttributeStrategy;
class GetFromUrlParamAttribute extends GetParamsFromAttributeStrategy
{
protected string $attributeName = UrlParam::class;
}

View File

@@ -23,12 +23,7 @@ class GetFromUrlParamTag extends Strategy
{
foreach ($endpointData->method->getParameters() as $param) {
$paramType = $param->getType();
if ($paramType === null) {
continue;
}
if (class_exists(ReflectionUnionType::class)
&& $paramType instanceof ReflectionUnionType) {
if ($paramType === null || $paramType instanceof ReflectionUnionType) {
continue;
}

View File

@@ -332,16 +332,14 @@ class OutputTest extends BaseLaravelTest
/** @test */
public function generates_correct_url_params_from_resource_routes_and_model_binding_with_binded_interfaces()
{
$this->app->bind(TestPostBindedInterface::class, function(){
return new TestPost();
});
$this->app->bind(TestPostBindedInterface::class, fn() => new TestPost());
RouteFacade::resource('posts', TestPostBindedInterfaceController::class)->only('update');
config(['scribe.routes.0.match.prefixes' => ['*']]);
config(['scribe.routes.0.apply.response_calls.methods' => []]);
$this->artisan('scribe:generate');
$this->generate();
$group = Yaml::parseFile('.scribe/endpoints/00.yaml');
$this->assertEquals('posts/{slug}', $group['endpoints'][0]['uri']);

View File

@@ -0,0 +1,129 @@
<?php
namespace Knuckles\Scribe\Tests\Strategies\BodyParameters;
use Closure;
use Knuckles\Camel\Extraction\ExtractedEndpointData;
use Knuckles\Scribe\Attributes\BodyParam;
use Knuckles\Scribe\Extracting\Strategies\BodyParameters\GetFromBodyParamAttribute;
use Knuckles\Scribe\Tools\DocumentationConfig;
use PHPUnit\Framework\TestCase;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use ReflectionClass;
class GetFromBodyParamAttributeTest extends TestCase
{
use ArraySubsetAsserts;
/** @test */
public function can_fetch_from_bodyparam_attribute()
{
$endpoint = $this->endpoint(function (ExtractedEndpointData $e) {
$e->controller = new ReflectionClass(BodyParamAttributeTestController::class);
$e->method = $e->controller->getMethod('methodWithAttributes');
});
$results = $this->fetch($endpoint);
$this->assertArraySubset([
'location_id' => [
'type' => 'string',
'required' => true,
'description' => 'The id of the location.',
],
'user_id' => [
'type' => 'string',
'required' => true,
'description' => 'The id of the user.',
'example' => 'me',
],
'page' => [
'type' => 'integer',
'required' => false,
'description' => 'The page number.',
'example' => 4,
],
'with_type' => [
'type' => 'number',
'required' => false,
'description' => '',
'example' => 13.0,
],
'with_list_type' => [
'type' => 'integer[]',
'required' => false,
'description' => '',
],
'fields' => [
'type' => 'string[]',
'required' => false,
'description' => 'The fields.',
'example' => ['age', 'name']
],
'filters' => [
'type' => 'object',
'required' => false,
'description' => 'The filters.',
],
'filters.class' => [
'type' => 'number',
'required' => false,
'description' => 'Class.',
'example' => 11.0
],
'filters.other' => [
'type' => 'string',
'required' => true,
'description' => 'Other things.',
],
'noExampleNoDescription' => [
'type' => 'string',
'required' => true,
'description' => '',
'example' => null
],
'noExample' => [
'type' => 'string',
'required' => true,
'description' => 'Something',
'example' => null
],
], $results);
}
protected function fetch($endpoint): array
{
$strategy = new GetFromBodyParamAttribute(new DocumentationConfig([]));
return $strategy($endpoint, []);
}
protected function endpoint(Closure $configure): ExtractedEndpointData
{
$endpoint = new class extends ExtractedEndpointData {
public function __construct(array $parameters = []) {}
};
$configure($endpoint);
return $endpoint;
}
}
#[BodyParam("user_id", description: "Will be overriden.")]
#[BodyParam("location_id", description: "The id of the location.")]
class BodyParamAttributeTestController
{
#[BodyParam("user_id", description: "The id of the user.", example: "me")]
#[BodyParam("page", 'integer', description: "The page number.", required: false, example: 4)]
#[BodyParam("with_type", "number", example: 13.0, required: false)]
#[BodyParam("with_list_type", type: "int[]", required: false)]
#[BodyParam("fields", "string[]", "The fields.", required: false, example: ["age", "name"])]
#[BodyParam("filters", "object", "The filters. ", required: false)]
#[BodyParam("filters.class", "double", required: false, example: 11.0, description: "Class.")]
#[BodyParam("filters.other", "string", description: "Other things.")]
#[BodyParam("noExampleNoDescription", example: "No-example")]
#[BodyParam("noExample", description: "Something", example: "No-example")]
public function methodWithAttributes()
{
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Knuckles\Scribe\Tests\Strategies\QueryParameters;
use Closure;
use Knuckles\Camel\Extraction\ExtractedEndpointData;
use Knuckles\Scribe\Attributes\QueryParam;
use Knuckles\Scribe\Extracting\Strategies\QueryParameters\GetFromQueryParamAttribute;
use Knuckles\Scribe\Tools\DocumentationConfig;
use PHPUnit\Framework\TestCase;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use ReflectionClass;
class GetFromQueryParamAttributeTest extends TestCase
{
use ArraySubsetAsserts;
/** @test */
public function can_fetch_from_queryparam_attribute()
{
$endpoint = $this->endpoint(function (ExtractedEndpointData $e) {
$e->controller = new ReflectionClass(QueryParamAttributeTestController::class);
$e->method = $e->controller->getMethod('methodWithAttributes');
});
$results = $this->fetch($endpoint);
$this->assertArraySubset([
'location_id' => [
'type' => 'string',
'required' => true,
'description' => 'The id of the location.',
],
'user_id' => [
'type' => 'string',
'required' => true,
'description' => 'The id of the user.',
'example' => 'me',
],
'page' => [
'type' => 'integer',
'required' => false,
'description' => 'The page number.',
'example' => 4,
],
'with_type' => [
'type' => 'number',
'required' => false,
'description' => '',
'example' => 13.0,
],
'with_list_type' => [
'type' => 'integer[]',
'required' => false,
'description' => '',
],
'fields' => [
'type' => 'string[]',
'required' => false,
'description' => 'The fields.',
'example' => ['age', 'name']
],
'filters' => [
'type' => 'object',
'required' => false,
'description' => 'The filters.',
],
'filters.class' => [
'type' => 'number',
'required' => false,
'description' => 'Class.',
'example' => 11.0
],
'filters.other' => [
'type' => 'string',
'required' => true,
'description' => 'Other things.',
],
'noExampleNoDescription' => [
'type' => 'string',
'required' => true,
'description' => '',
'example' => null
],
'noExample' => [
'type' => 'string',
'required' => true,
'description' => 'Something',
'example' => null
],
], $results);
}
protected function fetch($endpoint): array
{
$strategy = new GetFromQueryParamAttribute(new DocumentationConfig([]));
return $strategy($endpoint, []);
}
protected function endpoint(Closure $configure): ExtractedEndpointData
{
$endpoint = new class extends ExtractedEndpointData {
public function __construct(array $parameters = []) {}
};
$configure($endpoint);
return $endpoint;
}
}
#[QueryParam("user_id", description: "Will be overriden.")]
#[QueryParam("location_id", description: "The id of the location.")]
class QueryParamAttributeTestController
{
#[QueryParam("user_id", description: "The id of the user.", example: "me")]
#[QueryParam("page", 'integer', description: "The page number.", required: false, example: 4)]
#[QueryParam("with_type", "number", example: 13.0, required: false)]
#[QueryParam("with_list_type", type: "int[]", required: false)]
#[QueryParam("fields", "string[]", "The fields.", required: false, example: ["age", "name"])]
#[QueryParam("filters", "object", "The filters. ", required: false)]
#[QueryParam("filters.class", "double", required: false, example: 11.0, description: "Class.")]
#[QueryParam("filters.other", "string", description: "Other things.")]
#[QueryParam("noExampleNoDescription", example: "No-example")]
#[QueryParam("noExample", description: "Something", example: "No-example")]
public function methodWithAttributes()
{
}
}

View File

@@ -2,7 +2,7 @@
namespace Knuckles\Scribe\Tests\Strategies\UrlParameters;
use Illuminate\Routing\Route;
use Closure;
use Illuminate\Routing\Router;
use Knuckles\Camel\Extraction\ExtractedEndpointData;
use Knuckles\Scribe\Extracting\Strategies\UrlParameters\GetFromLaravelAPI;
@@ -18,17 +18,8 @@ class GetFromLaravelAPITest extends BaseLaravelTest
/** @test */
public function can_fetch_from_url()
{
$endpoint = new class extends ExtractedEndpointData {
public function __construct(array $parameters = [])
{
$this->method = new \ReflectionMethod(TestController::class, 'withInjectedModel');
$this->route = app(Router::class)->addRoute(['GET'], "users/{id}", ['uses' => [TestController::class, 'withInjectedModel']]);
$this->uri = $this->route->uri;
}
};
$strategy = new GetFromLaravelAPI(new DocumentationConfig([]));
$results = $strategy($endpoint, []);
$endpoint = $this->endpointForRoute("users/{id}", TestController::class, 'withInjectedModel');
$results = $this->fetch($endpoint);
$this->assertArraySubset([
"name" => "id",
@@ -42,17 +33,8 @@ class GetFromLaravelAPITest extends BaseLaravelTest
/** @test */
public function can_infer_description_from_url()
{
$endpoint = new class extends ExtractedEndpointData {
public function __construct(array $parameters = [])
{
$this->method = new \ReflectionMethod(TestController::class, 'dummy');
$this->route = app(Router::class)->addRoute(['GET'], "everything/{cat_id}", ['uses' => [TestController::class, 'dummy']]);
$this->uri = $this->route->uri;
}
};
$strategy = new GetFromLaravelAPI(new DocumentationConfig([]));
$results = $strategy($endpoint, []);
$endpoint = $this->endpointForRoute("everything/{cat_id}", TestController::class, 'dummy');
$results = $this->fetch($endpoint);
$this->assertArraySubset([
"name" => "cat_id",
@@ -63,7 +45,7 @@ class GetFromLaravelAPITest extends BaseLaravelTest
$endpoint->route = app(Router::class)->addRoute(['GET'], 'dogs/{id}', ['uses' => [TestController::class, 'dummy']]);;
$endpoint->uri = $endpoint->route->uri;
$results = $strategy($endpoint, []);
$results = $this->fetch($endpoint);
$this->assertArraySubset([
"name" => "id",
@@ -76,20 +58,14 @@ class GetFromLaravelAPITest extends BaseLaravelTest
/** @test */
public function can_infer_example_from_wheres()
{
$endpoint = new class extends ExtractedEndpointData {
public function __construct(array $parameters = [])
{
$this->method = new \ReflectionMethod(TestController::class, 'dummy');
$route = app(Router::class)->addRoute(['GET'], "everything/{cat_id}", ['uses' => [TestController::class, 'dummy']]);
$this->regex = '/catz\d+-\d/';
$this->route = $route->where('cat_id', $this->regex);
$this->uri = $this->route->uri;
}
};
$strategy = new GetFromLaravelAPI(new DocumentationConfig([]));
$results = $strategy($endpoint, []);
$regex = '/catz\d+-\d/';
$endpoint = $this->endpoint(function (ExtractedEndpointData $e) use ($regex) {
$e->method = new \ReflectionMethod(TestController::class, 'dummy');
$e->route = app(Router::class)->addRoute(['GET'], "everything/{cat_id}", ['uses' => [TestController::class, 'dummy']])
->where('cat_id', $regex);
$e->uri = $e->route->uri;
});
$results = $this->fetch($endpoint);
$this->assertArraySubset([
"name" => "cat_id",
@@ -97,26 +73,14 @@ class GetFromLaravelAPITest extends BaseLaravelTest
"required" => true,
"type" => "string",
], $results['cat_id']);
$this->assertMatchesRegularExpression($endpoint->regex, $results['cat_id']['example']);
$this->assertMatchesRegularExpression($regex, $results['cat_id']['example']);
}
/** @test */
public function can_infer_data_from_field_bindings()
{
$strategy = new GetFromLaravelAPI(new DocumentationConfig([]));
$endpoint = new class extends ExtractedEndpointData {
public function __construct(array $parameters = [])
{
$this->method = new \ReflectionMethod(TestController::class, 'dummy');
$route = app(Router::class)->addRoute(['GET'], "audio/{audio:slug}", ['uses' => [TestController::class, 'dummy']]);
$this->route = $route;
$this->uri = $route->uri;
}
};
$results = $strategy($endpoint, []);
$endpoint = $this->endpointForRoute("audio/{audio:slug}", TestController::class, 'dummy');
$results = $this->fetch($endpoint);
$this->assertArraySubset([
"name" => "audio",
@@ -125,18 +89,8 @@ class GetFromLaravelAPITest extends BaseLaravelTest
"type" => "string",
], $results['audio']);
$endpoint = new class extends ExtractedEndpointData {
public function __construct(array $parameters = [])
{
$this->method = new \ReflectionMethod(TestController::class, 'withInjectedModel');
$route = app(Router::class)->addRoute(['GET'], "users/{user:id}", ['uses' => [TestController::class, 'withInjectedModel']]);
$this->route = $route;
$this->uri = $route->uri;
}
};
$results = $strategy($endpoint, []);
$endpoint = $this->endpointForRoute("users/{user:id}", TestController::class, 'withInjectedModel');
$results = $this->fetch($endpoint);
$this->assertArraySubset([
"name" => "user",
@@ -145,4 +99,31 @@ class GetFromLaravelAPITest extends BaseLaravelTest
"type" => "integer",
], $results['user']);
}
protected function endpointForRoute($path, $controller, $method): ExtractedEndpointData
{
return $this->endpoint(function (ExtractedEndpointData $e) use ($path, $method, $controller) {
$e->method = new \ReflectionMethod($controller, $method);
$e->route = app(Router::class)->addRoute(['GET'], $path, ['uses' => [$controller, $method]]);
$e->uri = $e->route->uri;
});
}
protected function endpoint(Closure $configure): ExtractedEndpointData
{
$endpoint = new class extends ExtractedEndpointData {
public function __construct(array $parameters = [])
{
}
};
$configure($endpoint);
return $endpoint;
}
protected function fetch($endpoint): array
{
$strategy = new GetFromLaravelAPI(new DocumentationConfig([]));
return $strategy($endpoint, []);
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Knuckles\Scribe\Tests\Strategies\UrlParameters;
use Closure;
use Knuckles\Camel\Extraction\ExtractedEndpointData;
use Knuckles\Scribe\Attributes\UrlParam;
use Knuckles\Scribe\Extracting\Strategies\UrlParameters\GetFromUrlParamAttribute;
use Knuckles\Scribe\Tools\DocumentationConfig;
use PHPUnit\Framework\TestCase;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use ReflectionClass;
use ReflectionFunction;
class GetFromUrlParamAttributeTest extends TestCase
{
use ArraySubsetAsserts;
/** @test */
public function can_fetch_from_urlparam_attribute()
{
$endpoint = $this->endpoint(function (ExtractedEndpointData $e) {
$e->controller = new ReflectionClass(UrlParamAttributeTestController::class);
$e->method = $e->controller->getMethod('methodWithAttributes');
});
$results = $this->fetch($endpoint);
$this->assertArraySubset([
'id' => [
'type' => 'string',
'required' => true,
'description' => 'The id of the order.',
],
'lang' => [
'type' => 'string',
'required' => false,
'description' => 'Will be inherited.',
],
'withType' => [
'type' => 'number',
'required' => false,
'description' => 'With type, maybe.',
],
'withTypeDefinitely' => [
'type' => 'integer',
'required' => true,
'description' => 'With type.',
],
'barebones' => [
'type' => 'string',
'required' => true,
'description' => '',
],
'barebonesType' => [
'type' => 'number',
'required' => true,
'description' => '',
],
'barebonesOptional' => [
'type' => 'string',
'required' => false,
'description' => '',
],
'withExampleOnly' => [
'type' => 'string',
'required' => true,
'description' => '',
'example' => '12',
],
'withExampleOnlyButTyped' => [
'type' => 'integer',
'required' => true,
'description' => '',
'example' => 12
],
'noExampleNoDescription' => [
'type' => 'string',
'required' => true,
'description' => '',
'example' => null
],
'noExample' => [
'type' => 'string',
'required' => true,
'description' => 'Something',
'example' => null
],
], $results);
}
/** @test */
public function can_fetch_from_urlparam_attribute_on_closure()
{
$endpoint = $this->endpoint(function (ExtractedEndpointData $e) {
$e->controller = null;
$e->method = new ReflectionFunction('\Knuckles\Scribe\Tests\Strategies\UrlParameters\functionWithAttributes');
});
$results = $this->fetch($endpoint);
$this->assertArraySubset([
'a_parameter' => [
'type' => 'string',
'required' => false,
'description' => 'Described',
'example' => 'en',
],
], $results);
}
protected function fetch($endpoint): array
{
$strategy = new GetFromUrlParamAttribute(new DocumentationConfig([]));
return $strategy($endpoint, []);
}
protected function endpoint(Closure $configure): ExtractedEndpointData
{
$endpoint = new class extends ExtractedEndpointData {
public function __construct(array $parameters = []) {}
};
$configure($endpoint);
return $endpoint;
}
}
#[UrlParam("id", description: "Will be overriden.")]
#[UrlParam("lang", description: "Will be inherited.", required: false)]
class UrlParamAttributeTestController
{
#[UrlParam("id", description: "The id of the order.")]
#[UrlParam("withType", required: false, type: "number", description: "With type, maybe.")]
#[UrlParam("withTypeDefinitely", "integer", "With type.")]
#[UrlParam("barebones")]
#[UrlParam("barebonesType", type: "number")]
#[UrlParam("barebonesOptional", required: false)]
#[UrlParam("withExampleOnly", example: "12")]
#[UrlParam("withExampleOnlyButTyped", type: "int", example: 12)]
#[UrlParam("noExampleNoDescription", example: "No-example.")]
#[UrlParam("noExample", description: "Something", example: "No-example")]
public function methodWithAttributes()
{
}
}
#[UrlParam("a_parameter", required: false, type: "string", description: "Described", example: "en")]
function functionWithAttributes()
{
}