Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"php-cs-fixer.executablePath": "${workspaceFolder}/vendor/bin/php-cs-fixer"
}
"php-cs-fixer.executablePath": "${workspaceFolder}/vendor/bin/php-cs-fixer",
"php.version": "8.3.1"
}
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"tomasvotruba/type-coverage": "^0.2.1"
},
"suggest": {
"hammerstone/fast-paginate": "Improves jsonApiPaginate method performance (SQL database query pagination)",
"aaronfrancis/fast-paginate": "Improves jsonApiPaginate method performance (SQL database query pagination)",
"laravel/scout": "Integrate search with JsonApiResponse using its search method"
},
"minimum-stability": "dev",
Expand Down
29 changes: 21 additions & 8 deletions config/apiable.php
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
<?php

use OpenSoutheners\LaravelApiable\Enums\ResponseType;
use OpenSoutheners\LaravelApiable\Http\AllowedFilter;
use OpenSoutheners\LaravelApiable\Http\AllowedSort;

return [

/**
* Resource type model map.
*
* @see https://docs.opensoutheners.com/laravel-apiable/guide/#getting-started
*/
'resource_type_map' => [],

/**
* Default options for request query filters, sorts, etc.
*
* @see https://docs.opensoutheners.com/laravel-apiable/guide/requests.html
*/
'requests' => [
'validate' => ! ((bool) env('APIABLE_DEV_MODE', false)),

'validate_params' => false,

'filters' => [
Expand All @@ -37,7 +33,7 @@
*/
'responses' => [
'formatting' => [
'type' => 'application/vnd.api+json',
'type' => ResponseType::JsonApi->value,
'force' => false,
],

Expand All @@ -54,4 +50,21 @@
'include_ids_on_attributes' => false,
],

/**
* Default options for responses like: normalize relations names, include allowed filters and sorts, etc.
*
* @see https://docs.opensoutheners.com/laravel-apiable/guide/documentation.html
*/
'documentation' => [

'markdown' => [
'base_path' => 'storage/exports/markdown',
],

'postman' => [
'base_path' => 'storage/exports',
],

],

];
12 changes: 11 additions & 1 deletion src/Attributes/AppendsQueryParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@
namespace OpenSoutheners\LaravelApiable\Attributes;

use Attribute;
use OpenSoutheners\LaravelApiable\ServiceProvider;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class AppendsQueryParam extends QueryParam
{
public function __construct(public string $type, public array $attributes)
public function __construct(public string $type, public array $attributes, public string $description = '')
{
//
}

public function getTypeAsResource(): string
{
if (! str_contains($this->type, '\\')) {
return $this->type;
}

return ServiceProvider::getTypeForModel($this->type);
}
}
12 changes: 11 additions & 1 deletion src/Attributes/FieldsQueryParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@
namespace OpenSoutheners\LaravelApiable\Attributes;

use Attribute;
use OpenSoutheners\LaravelApiable\ServiceProvider;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class FieldsQueryParam extends QueryParam
{
public function __construct(public string $type, public array $fields)
public function __construct(public string $type, public array $fields, public string $description = '')
{
//
}

public function getTypeAsResource(): string
{
if (! str_contains($this->type, '\\')) {
return $this->type;
}

return ServiceProvider::getTypeForModel($this->type);
}
}
63 changes: 61 additions & 2 deletions src/Attributes/FilterQueryParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,71 @@
namespace OpenSoutheners\LaravelApiable\Attributes;

use Attribute;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use OpenSoutheners\LaravelApiable\Http\QueryParamValueType;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class FilterQueryParam extends QueryParam
{
public function __construct(public string $attribute, public int|array|null $type = null, public $values = '*')
{
public function __construct(
public string $attribute,
public int|array|null $type = null,
public string|array|QueryParamValueType $values = '*',
public string $description = ''
) {
//
}

public function getDataType(): QueryParamValueType|array
{
if ($this->values instanceof QueryParamValueType) {
return $this->values;
}

if (is_array($this->values)) {
return array_unique(
array_map(
fn ($value) => $this->assertDataType($value),
$this->values
)
);
}

return $this->assertDataType($this->values);
}

protected function assertDataType(mixed $value): QueryParamValueType
{
if (is_numeric($value)) {
return QueryParamValueType::Integer;
}

if ($this->isTimestamp($value)) {
return QueryParamValueType::Timestamp;
}

if (in_array($value, ['true', 'false'])) {
return QueryParamValueType::Boolean;
}

if (Str::isJson($value)) {
return QueryParamValueType::Object;
}

// TODO: Array like "param[0]=foo&param[1]=bar"...

return QueryParamValueType::String;
}

protected function isTimestamp(mixed $value): bool
{
try {
Carbon::parse($value);

return true;
} catch (\Exception $e) {
return false;
}
}
}
14 changes: 14 additions & 0 deletions src/Attributes/ForceAppendAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace OpenSoutheners\LaravelApiable\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class ForceAppendAttribute
{
public function __construct(public string|array $type, public string|array $attributes)
{
//
}
}
2 changes: 1 addition & 1 deletion src/Attributes/IncludeQueryParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class IncludeQueryParam extends QueryParam
{
public function __construct(public string|array $relationships)
public function __construct(public string|array $relationships, public string $description = '')
{
//
}
Expand Down
2 changes: 1 addition & 1 deletion src/Attributes/QueryParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace OpenSoutheners\LaravelApiable\Attributes;

class QueryParam
abstract class QueryParam
{
//
}
3 changes: 2 additions & 1 deletion src/Attributes/SearchFilterQueryParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
namespace OpenSoutheners\LaravelApiable\Attributes;

use Attribute;
use OpenSoutheners\LaravelApiable\Http\QueryParamValueType;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final class SearchFilterQueryParam extends QueryParam
{
public function __construct(public string $attribute, public $values = '*')
public function __construct(public string $attribute, public string|array|QueryParamValueType $values = '*', public string $description = '')
{
//
}
Expand Down
2 changes: 1 addition & 1 deletion src/Attributes/SearchQueryParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final class SearchQueryParam extends QueryParam
{
public function __construct(public bool $allowSearch = true)
public function __construct(public bool $allowSearch = true, public string $description = '')
{
//
}
Expand Down
2 changes: 1 addition & 1 deletion src/Attributes/SortQueryParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class SortQueryParam extends QueryParam
{
public function __construct(public string $attribute, public ?int $direction = AllowedSort::BOTH)
public function __construct(public string $attribute, public ?int $direction = AllowedSort::BOTH, public string $description = '')
{
//
}
Expand Down
Empty file added src/Config.php
Empty file.
123 changes: 123 additions & 0 deletions src/Console/ApiableDocgenCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

namespace OpenSoutheners\LaravelApiable\Console;

use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Routing\Route;
use Illuminate\Routing\RouteCollection;
use Illuminate\Routing\Router;
use Illuminate\Support\Str;
use OpenSoutheners\LaravelApiable\Documentation\Generator;

class ApiableDocgenCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'apiable:docgen
{--exclude= : Exclude routes containing the following list on their URI paths (comma separated)}
{--only= : Generate documentation only for routes containing the following on their URI paths}
{--markdown : Generate documentation in raw and reusable Markdown}
{--postman : Generate documentation as Postman collection}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate API documentation based on JSON:API endpoints.';

/**
* Create a new console command instance.
*
* @return void
*/
public function __construct(
protected Router $router,
protected Generator $generator,
protected Filesystem $filesystem,
protected array $files = [],
protected array $resources = [],
protected array $endpoints = []
) {
parent::__construct();
}

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$exportFormat = $this->askForExportFormat();

$this->generator->generate();

// TODO: Auth event with Sanctum or Passport?
match ($exportFormat) {
'postman' => $this->exportEndpointsToPostman(),
'markdown' => $this->exportEndpointsToMarkdown(),
default => null
};

foreach ($this->files as $path => $content) {
$this->filesystem->ensureDirectoryExists(Str::beforeLast($path, '/'));

$this->filesystem->put($path, $content);
}

$this->info("Export successfully to {$exportFormat}");

return 0;
}

public function exportEndpointsToMarkdown()
{
$this->files = array_merge($this->files, $this->generator->toMarkdown());
}

// TODO: Update with new array data structure from fetchRoutes
protected function exportEndpointsToPostman(): void
{
$this->files[config('apiable.documentation.postman.base_path').'/documentation.postman_collection.json'] = $this->generator->toPostmanCollection();
}

protected function filterRoutesToDocument(RouteCollection $routes)
{
$filterOnlyBy = $this->option('only');

return array_filter(iterator_to_array($routes), function (Route $route) use ($filterOnlyBy) {
$hasBeenExcluded = Str::is(array_merge(explode(',', $this->option('exclude')), [
'_debugbar/*', '_ignition/*', 'nova-api/*', 'nova/*', 'nova',
]), $route->uri());

if ($hasBeenExcluded) {
return false;
}

if ($filterOnlyBy) {
return Str::is($filterOnlyBy, $route->uri());
}

return true;
});
}

protected function askForExportFormat()
{
$postman = $this->option('postman');
$markdown = $this->option('markdown');

$formatOptions = compact('postman', 'markdown');

if (empty($formatOptions)) {
$option = $this->askWithCompletion('Export API documentation using format', array_keys($formatOptions));
}

return head(array_keys(array_filter($formatOptions)));
}
}
Loading
Loading