Skip to content

Commit

Permalink
Added support for manually annotating request parameters using attrib…
Browse files Browse the repository at this point in the history
…utes (#686)

* parameters annotations wip implementation

* parameter extractors introduced so attributes extractor can work on top of third party packages extractors

* Fix styling

* parameter defaults

---------

Co-authored-by: romalytvynenko <[email protected]>
  • Loading branch information
romalytvynenko and romalytvynenko authored Jan 18, 2025
1 parent 6e0232c commit d3546e8
Show file tree
Hide file tree
Showing 26 changed files with 822 additions and 193 deletions.
25 changes: 25 additions & 0 deletions src/Attributes/CookieParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Dedoc\Scramble\Attributes;

use Attribute;

#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class CookieParameter extends Parameter
{
public readonly bool $required;

public function __construct(
string $name,
?string $description = null,
?bool $required = null,
$deprecated = false,
?string $type = null,
bool $infer = true,
mixed $default = new MissingValue,
mixed $example = new MissingValue,
array $examples = [],
) {
parent::__construct('cookie', $name, $description, $required, $deprecated, $type, $infer, $default, $example, $examples);
}
}
30 changes: 30 additions & 0 deletions src/Attributes/Example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Dedoc\Scramble\Attributes;

use Dedoc\Scramble\Support\Generator\Example as OpenApiExample;
use Dedoc\Scramble\Support\Generator\MissingValue as OpenApiMissingValue;

class Example
{
public function __construct(
public mixed $value = new MissingValue,
public ?string $summary = null,
public ?string $description = null,
public ?string $externalValue = null,
) {}

public static function toOpenApiExample(mixed $example)
{
if ($example instanceof static) {
return new OpenApiExample(
value: $example->value instanceof MissingValue ? new OpenApiMissingValue : $example->value,
summary: $example->summary,
description: $example->description,
externalValue: $example->externalValue,
);
}

return $example;
}
}
25 changes: 25 additions & 0 deletions src/Attributes/HeaderParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Dedoc\Scramble\Attributes;

use Attribute;

#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class HeaderParameter extends Parameter
{
public readonly bool $required;

public function __construct(
string $name,
?string $description = null,
?bool $required = null,
$deprecated = false,
?string $type = null,
bool $infer = true,
mixed $default = new MissingValue,
mixed $example = new MissingValue,
array $examples = [],
) {
parent::__construct('header', $name, $description, $required, $deprecated, $type, $infer, $default, $example, $examples);
}
}
5 changes: 5 additions & 0 deletions src/Attributes/MissingValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Dedoc\Scramble\Attributes;

class MissingValue {}
31 changes: 31 additions & 0 deletions src/Attributes/Parameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Dedoc\Scramble\Attributes;

use Attribute;

#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class Parameter
{
public readonly bool $required;

/**
* @param 'query'|'path'|'header'|'cookie' $in
* @param scalar|array|object|MissingValue $example
* @param array<string, Example> $examples The key is a distinct name and the value is an example object.
*/
public function __construct(
public readonly string $in,
public readonly string $name,
public readonly ?string $description = null,
?bool $required = null,
public bool $deprecated = false,
public ?string $type = null,
public bool $infer = true,
public mixed $default = new MissingValue,
public mixed $example = new MissingValue,
public array $examples = [],
) {
$this->required = $required !== null ? $required : $this->in === 'path';
}
}
25 changes: 25 additions & 0 deletions src/Attributes/PathParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Dedoc\Scramble\Attributes;

use Attribute;

#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class PathParameter extends Parameter
{
public readonly bool $required;

public function __construct(
string $name,
?string $description = null,
?bool $required = null,
$deprecated = false,
?string $type = null,
bool $infer = true,
mixed $default = new MissingValue,
mixed $example = new MissingValue,
array $examples = [],
) {
parent::__construct('path', $name, $description, $required, $deprecated, $type, $infer, $default, $example, $examples);
}
}
25 changes: 25 additions & 0 deletions src/Attributes/QueryParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Dedoc\Scramble\Attributes;

use Attribute;

#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class QueryParameter extends Parameter
{
public readonly bool $required;

public function __construct(
string $name,
?string $description = null,
?bool $required = null,
$deprecated = false,
?string $type = null,
bool $infer = true,
mixed $default = new MissingValue,
mixed $example = new MissingValue,
array $examples = [],
) {
parent::__construct('query', $name, $description, $required, $deprecated, $type, $infer, $default, $example, $examples);
}
}
57 changes: 57 additions & 0 deletions src/Configuration/ParametersExtractors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Dedoc\Scramble\Configuration;

use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\FormRequestParametersExtractor;
use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\ValidateCallParametersExtractor;
use Illuminate\Support\Arr;

class ParametersExtractors
{
protected array $extractors = [];

protected array $appends = [];

protected array $prepends = [];

public function append(array|string $extractor)
{
$this->appends = array_merge(
$this->appends,
Arr::wrap($extractor)
);

return $this;
}

public function prepend(array|string $extractor)
{
$this->prepends = array_merge(
$this->prepends,
Arr::wrap($extractor)
);

return $this;
}

public function use(array $extractors)
{
$this->extractors = $extractors;

return $this;
}

public function all(): array
{
$base = $this->extractors ?: [
FormRequestParametersExtractor::class,
ValidateCallParametersExtractor::class,
];

return array_values(array_unique([
...$this->prepends,
...$base,
...$this->appends,
]));
}
}
4 changes: 1 addition & 3 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ public function setThrowExceptions(bool $throwExceptions): static

public function __invoke(?GeneratorConfig $config = null)
{
$config ??= (new GeneratorConfig(config('scramble')))
->routes(Scramble::$routeResolver)
->afterOpenApiGenerated(Scramble::$openApiExtender);
$config ??= Scramble::getGeneratorConfig(Scramble::DEFAULT_API);

$openApi = $this->makeOpenApi($config);
$context = new OpenApiContext($openApi, $config);
Expand Down
16 changes: 16 additions & 0 deletions src/GeneratorConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Dedoc\Scramble;

use Closure;
use Dedoc\Scramble\Configuration\ParametersExtractors;
use Illuminate\Routing\Route;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
Expand All @@ -13,6 +14,7 @@ public function __construct(
private array $config = [],
private ?Closure $routeResolver = null,
private array $afterOpenApiGenerated = [],
public readonly ParametersExtractors $parametersExtractors = new ParametersExtractors,
) {}

public function config(array $config)
Expand Down Expand Up @@ -56,6 +58,20 @@ public function afterOpenApiGenerated(?callable $afterOpenApiGenerated = null)
return $this;
}

public function useConfig(array $config): static
{
$this->config = $config;

return $this;
}

public function withParametersExtractors(callable $callback): static
{
$callback($this->parametersExtractors);

return $this;
}

public function get(string $key, mixed $default = null)
{
return Arr::get($this->config, $key, $default);
Expand Down
24 changes: 14 additions & 10 deletions src/Scramble.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Dedoc\Scramble;

use Dedoc\Scramble\Configuration\ParametersExtractors;
use Dedoc\Scramble\Extensions\ExceptionToResponseExtension;
use Dedoc\Scramble\Extensions\OperationExtension;
use Dedoc\Scramble\Extensions\TypeToSchemaExtension;
Expand All @@ -19,12 +20,10 @@

class Scramble
{
public static $routeResolver = null;
const DEFAULT_API = 'default';

public static $tagResolver = null;

public static $openApiExtender = null;

public static bool $defaultRoutesIgnored = false;

/**
Expand Down Expand Up @@ -66,36 +65,41 @@ public static function ignoreDefaultRoutes(): void
public static function registerApi(string $name, array $config = []): GeneratorConfig
{
static::$apis[$name] = $generatorConfig = new GeneratorConfig(
config: array_merge(config('scramble'), $config),
config: array_merge(config('scramble') ?: [], $config),
parametersExtractors: isset(static::$apis['default'])
? static::$apis['default']->parametersExtractors
: new ParametersExtractors,
);

// By default, afterOpenApiGenerated is the same for all APIs.
$generatorConfig->afterOpenApiGenerated(Scramble::$openApiExtender);

return $generatorConfig;
}

public static function configure(string $api = self::DEFAULT_API): GeneratorConfig
{
return static::getGeneratorConfig($api);
}

/**
* Update open api document before finally rendering it.
*
* @deprecated
*/
public static function extendOpenApi(callable $openApiExtender)
{
static::$openApiExtender = $openApiExtender;
static::afterOpenApiGenerated($openApiExtender);
}

/**
* Update Open API document before finally rendering it.
*/
public static function afterOpenApiGenerated(callable $afterOpenApiGenerated)
{
static::$openApiExtender = $afterOpenApiGenerated;
static::configure()->afterOpenApiGenerated($afterOpenApiGenerated);
}

public static function routes(callable $routeResolver)
{
static::$routeResolver = $routeResolver;
static::configure()->routes($routeResolver);
}

/**
Expand Down
Loading

0 comments on commit d3546e8

Please sign in to comment.