Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #1

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
862e38b
improve class analisys logic
Aug 2, 2024
d9ce4fe
improve class analisys logic
Aug 2, 2024
504cf82
add ability for model to resolve casts
Aug 7, 2024
a122395
improve reference, add static make method
Aug 8, 2024
3f7a34b
improve: improve validation rules extraction from the 'rules()' metho…
Aug 30, 2024
3381d5b
improve: check the method parameter is actually part of the route whi…
Sep 2, 2024
5552e70
improve: replace api/ correctly in the rendered doc paths
Sep 4, 2024
bee6b74
impove: unhardcode reference and type transformer
Sep 5, 2024
5547a2a
feat(param-references): Add ability to include params as references
Sep 17, 2024
a1e68d2
feat(enum-tag): Add @enum tag support
Sep 18, 2024
f59d7d0
feat(type-ad-hoc-description): Add type ad-hoc description support
Sep 19, 2024
b711602
feat(type-ad-hoc-description): Add type ad-hoc description support
Sep 19, 2024
69faabe
feat(type-ad-hoc-default): Add ad-hoc default values support
Sep 19, 2024
2118596
feat(optional-values): Add optional value automark if it's nullable
Sep 19, 2024
3e4fa51
feat(optional-values): Add optional value automark if it's nullable
Sep 19, 2024
a2465dd
feat(arrqay-items): Add array items support, remove hadrcode for addi…
Sep 19, 2024
1ebf540
feat(array-items): Add array items support, remove hadrcode for addit…
Sep 19, 2024
a332108
feat(array-items): Add array items support, remove hadrcode for addit…
Sep 19, 2024
15fa806
feat(array-object): Add array-object type support
Sep 19, 2024
9997901
feat(array-object): Add array-object type support
Sep 19, 2024
24c1d9e
feat(array-object): Add array object type support
Sep 19, 2024
43c1ebf
feat(array-object): Add array-object type support
Sep 19, 2024
bb59b8f
feat(array-object): Add array-object type support
Sep 19, 2024
f04de87
feat(array-object): Add array-object type support
Sep 19, 2024
3b5b54e
feat(attribute-title): Add attribute title support
Sep 20, 2024
ce40ec3
feat(attribute-title): Add array object attribute title support
Sep 20, 2024
b20634a
feat(array-object): Improve and optimize the array object type rendering
Sep 20, 2024
7f4efce
feat(array-object): Improve and optimize the array object type rendering
Sep 20, 2024
7461271
Merge branch 'main' of github.com:yvoitenko/scramble into develop
Sep 20, 2024
d2a93b2
resolve: sync with parent
Sep 20, 2024
53e1958
resolve: add legacy property fetch type parser
Sep 20, 2024
e949994
feat(empty-request-body): Add empty request body support regardless o…
Sep 25, 2024
534a6ed
feat(tweak-file-upload): Add file upload in OAS 3.1 format descriptio…
Oct 8, 2024
b182027
Merge pull request #2 from justcoded/feature/describe-file-upload-oas…
yvoitenko Oct 8, 2024
da60531
feat(default-values-from-docnode): Add default values from the docnod…
Oct 9, 2024
68a9fa4
Merge pull request #3 from yvoitenko/develop
yvoitenko Oct 9, 2024
cb622bc
resolve: sync with main
Oct 9, 2024
0bac1cd
feat(update-scramble): Use new scramble features
Oct 9, 2024
5be2969
Merge pull request #4 from justcoded/dev-sync-with-main
yvoitenko Oct 9, 2024
06c2f6c
chore: composer dependencies
Dec 13, 2024
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
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"php": "^8.1",
"illuminate/contracts": "^10.0|^11.0",
"myclabs/deep-copy": "^1.12",
"nikic/php-parser": "^5.0",
"nikic/php-parser": "^5.3",
"phpstan/phpdoc-parser": "^1.0",
"spatie/laravel-package-tools": "^1.9.2"
"spatie/laravel-package-tools": "^1.17.0"
},
"require-dev": {
"laravel/pint": "^v1.1.0",
Expand Down
74 changes: 41 additions & 33 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ class Generator
protected bool $throwExceptions = true;

public function __construct(
private TypeTransformer $transformer,
private OperationBuilder $operationBuilder,
private ServerFactory $serverFactory,
private FileParser $fileParser,
private Infer $infer
protected TypeTransformer $transformer,
protected OperationBuilder $operationBuilder,
protected ServerFactory $serverFactory,
protected FileParser $fileParser,
protected Infer $infer,
) {}

public function setThrowExceptions(bool $throwExceptions): static
Expand All @@ -44,6 +44,11 @@ public function setThrowExceptions(bool $throwExceptions): static
}

public function __invoke(?GeneratorConfig $config = null)
{
return $this->generate($config)->toArray();
}

public function generate(?GeneratorConfig $config = null): OpenApi
{
$config ??= (new GeneratorConfig(config('scramble')))
->routes(Scramble::$routeResolver)
Expand All @@ -64,21 +69,21 @@ public function __invoke(?GeneratorConfig $config = null)
$method = $route->methods()[0];
$action = $route->getAction('uses');

dump("Error when analyzing route '$method $route->uri' ($action): {$e->getMessage()} – ".($e->getFile().' on line '.$e->getLine()));
logger()->error("Error when analyzing route '$method $route->uri' ($action): {$e->getMessage()} – ".($e->getFile().' on line '.$e->getLine()));
dump("Error when analyzing route '$method $route->uri' ($action): {$e->getMessage()} – " . ($e->getFile() . ' on line ' . $e->getLine()));
logger()->error("Error when analyzing route '$method $route->uri' ($action): {$e->getMessage()} – " . ($e->getFile() . ' on line ' . $e->getLine()));
}

throw $e;
}
})
->filter() // Closure based routes are filtered out for now, right here
->sortBy(fn (Operation $o) => $o->tags[0] ?? $o->description)
->each(fn (Operation $operation) => $openApi->addPath(
->sortBy(fn(Operation $o) => $o->tags[0] ?? $o->description)
->each(fn(Operation $operation) => $openApi->addPath(
Path::make(
(string) Str::of($operation->path)
->replaceFirst($config->get('api_path', 'api'), '')
->trim('/')
)->addOperation($operation)
->replaceFirst(rtrim($config->get('api_path', 'api'), '/') . '/', '/')
->trim('/'),
)->addOperation($operation),
))
->toArray();

Expand All @@ -90,35 +95,36 @@ public function __invoke(?GeneratorConfig $config = null)
$afterOpenApiGenerated($openApi);
}

return $openApi->toArray();
return $openApi;
}

private function makeOpenApi(GeneratorConfig $config)
protected function makeOpenApi(GeneratorConfig $config)
{
$openApi = OpenApi::make('3.1.0')
->setComponents($this->transformer->getComponents())
->setInfo(
InfoObject::make($config->get('ui.title', $default = config('app.name')) ?: $default)
->setVersion($config->get('info.version', '0.0.1'))
->setDescription($config->get('info.description', ''))
->setDescription($config->get('info.description', '')),
);

[$defaultProtocol] = explode('://', url('/'));
$servers = $config->get('servers') ?: [
'' => ($domain = $config->get('api_domain'))
? $defaultProtocol.'://'.$domain.'/'.$config->get('api_path', 'api')
: $config->get('api_path', 'api'),
];
$servers = $config->get('servers')
?: [
'' => ($domain = $config->get('api_domain'))
? $defaultProtocol . '://' . $domain . '/' . $config->get('api_path', 'api')
: $config->get('api_path', 'api'),
];
foreach ($servers as $description => $url) {
$openApi->addServer(
$this->serverFactory->make(url($url ?: '/'), $description)
$this->serverFactory->make(url($url ?: '/'), $description),
);
}

return $openApi;
}

private function getRoutes(GeneratorConfig $config): Collection
protected function getRoutes(GeneratorConfig $config): Collection
{
return collect(RouteFacade::getRoutes())
->pipe(function (Collection $c) {
Expand All @@ -145,11 +151,11 @@ private function getRoutes(GeneratorConfig $config): Collection
return ! ($name = $route->getAction('as')) || ! Str::startsWith($name, 'scramble');
})
->filter($config->routes())
->filter(fn (Route $r) => $r->getAction('controller'))
->filter(fn(Route $r) => $r->getAction('controller'))
->values();
}

private function routeToOperation(OpenApi $openApi, Route $route, GeneratorConfig $config)
protected function routeToOperation(OpenApi $openApi, Route $route, GeneratorConfig $config)
{
$routeInfo = new RouteInfo($route, $this->fileParser, $this->infer);

Expand All @@ -164,7 +170,7 @@ private function routeToOperation(OpenApi $openApi, Route $route, GeneratorConfi
return $operation;
}

private function ensureSchemaTypes(Route $route, Operation $operation): void
protected function ensureSchemaTypes(Route $route, Operation $operation): void
{
if (! Scramble::getSchemaValidator()->hasRules()) {
return;
Expand All @@ -183,14 +189,16 @@ private function ensureSchemaTypes(Route $route, Operation $operation): void
}
}

private function createSchemaEnforceTraverser(Route $route)
protected function createSchemaEnforceTraverser(Route $route)
{
$traverser = new OpenApiTraverser([$visitor = new SchemaEnforceVisitor($route, $this->throwExceptions, $this->exceptions)]);
$traverser = new OpenApiTraverser([$visitor = new SchemaEnforceVisitor($route,
$this->throwExceptions,
$this->exceptions)]);

return [$traverser, $visitor];
}

private function moveSameAlternativeServersToPath(OpenApi $openApi)
protected function moveSameAlternativeServersToPath(OpenApi $openApi)
{
foreach (collect($openApi->paths)->groupBy('path') as $pathsGroup) {
if ($pathsGroup->isEmpty()) {
Expand All @@ -200,9 +208,9 @@ private function moveSameAlternativeServersToPath(OpenApi $openApi)
$operations = collect($pathsGroup->pluck('operations')->flatten());

$operationsHaveSameAlternativeServers = $operations->count()
&& $operations->every(fn (Operation $o) => count($o->servers))
&& $operations->every(fn(Operation $o) => count($o->servers))
&& $operations->unique(function (Operation $o) {
return collect($o->servers)->map(fn (Server $s) => $s->url)->join('.');
return collect($o->servers)->map(fn(Server $s) => $s->url)->join('.');
})->count() === 1;

if (! $operationsHaveSameAlternativeServers) {
Expand All @@ -217,9 +225,9 @@ private function moveSameAlternativeServersToPath(OpenApi $openApi)
}
}

private function setUniqueOperationId(OpenApi $openApi)
protected function setUniqueOperationId(OpenApi $openApi)
{
$names = new UniqueNamesOptionsCollection;
$names = new UniqueNamesOptionsCollection();

$this->foreachOperation($openApi, function (Operation $operation) use ($names) {
$names->push($operation->getAttribute('operationId'));
Expand All @@ -234,7 +242,7 @@ private function setUniqueOperationId(OpenApi $openApi)
});
}

private function foreachOperation(OpenApi $openApi, callable $callback)
protected function foreachOperation(OpenApi $openApi, callable $callback)
{
foreach (collect($openApi->paths)->groupBy('path') as $pathsGroup) {
if ($pathsGroup->isEmpty()) {
Expand Down
56 changes: 43 additions & 13 deletions src/Infer/Analyzer/ClassAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ private function shouldAnalyzeParentClass(ReflectionClass $parentClassReflection
* Classes from `vendor` aren't analyzed at the moment. Instead, it is up to developers to provide
* definitions for them using the dictionaries.
*/
return ! str_contains($parentClassReflection->getFileName(), DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR);

return ! str_contains($parentClassReflection->getFileName(),
DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR);
}

/**
Expand All @@ -51,7 +53,35 @@ public function analyze(string $name): ClassDefinition
// @todo: Here we still want to fire the event, so we can add some details to the definition.
$parentDefinition = new ClassDefinition($parentName = $classReflection->getParentClass()->name);

Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($parentDefinition->name, $parentDefinition));
Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($parentDefinition->name,
$parentDefinition));
}

$annotations = [];

if ($doc = $classReflection->getDocComment()) {
preg_match_all(
'/@(\w+)(\s\w+)?/',
$doc,
$annotations,
);

$annotations = array_map(
static fn($value) => empty($value) ? null : trim($value),
array_combine($annotations[1], $annotations[2]),
);
}

$attrs = $classReflection->getAttributes() ?? [];

foreach ($attrs as $index => $attr) {
if (! class_exists($attr->getName())) {
unset($attrs[$index]);

continue;
}

$attrs[$index] = new ($attr->getName())(...$attr->getArguments());
}

/*
Expand All @@ -61,9 +91,11 @@ public function analyze(string $name): ClassDefinition
$classDefinition = new ClassDefinition(
name: $name,
templateTypes: $parentDefinition?->templateTypes ?: [],
properties: array_map(fn ($pd) => clone $pd, $parentDefinition?->properties ?: []),
properties: array_map(fn($pd) => clone $pd, $parentDefinition?->properties ?: []),
methods: $parentDefinition?->methods ?: [],
parentFqn: $parentName ?? null,
annotations: $annotations,
attributes: $attrs,
);

/*
Expand All @@ -79,12 +111,12 @@ public function analyze(string $name): ClassDefinition
if ($reflectionProperty->isStatic()) {
$classDefinition->properties[$reflectionProperty->name] = new ClassPropertyDefinition(
type: $reflectionProperty->hasDefaultValue()
? (TypeHelper::createTypeFromValue($reflectionProperty->getDefaultValue()) ?: new UnknownType)
: new UnknownType,
? (TypeHelper::createTypeFromValue($reflectionProperty->getDefaultValue()) ?: new UnknownType())
: new UnknownType(),
);
} else {
$classDefinition->properties[$reflectionProperty->name] = new ClassPropertyDefinition(
type: $t = new TemplateType('T'.Str::studly($reflectionProperty->name)),
type: $t = new TemplateType('T' . Str::studly($reflectionProperty->name)),
defaultType: $reflectionProperty->hasDefaultValue()
? TypeHelper::createTypeFromValue($reflectionProperty->getDefaultValue())
: null,
Expand All @@ -94,23 +126,21 @@ public function analyze(string $name): ClassDefinition
}

foreach ($classReflection->getMethods() as $reflectionMethod) {
if ($reflectionMethod->class !== $name) {
continue;
}

$classDefinition->methods[$reflectionMethod->name] = new FunctionLikeDefinition(
new FunctionType(
$reflectionMethod->name,
name: $reflectionMethod->name,
arguments: [],
returnType: new UnknownType,
returnType: new UnknownType(),
),
definingClassName: $name,
);
}

$this->index->registerClassDefinition($classDefinition);

Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($classDefinition->name, $classDefinition));
Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(
new ClassDefinitionCreatedEvent($classDefinition->name, $classDefinition),
);

return $classDefinition;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Infer/Definition/ClassDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public function __construct(
/** @var array<string, FunctionLikeDefinition> $methods */
public array $methods = [],
public ?string $parentFqn = null,
public array $annotations = [],
public array $attributes = [],
) {}

public function isInstanceOf(string $className)
Expand Down
12 changes: 12 additions & 0 deletions src/Infer/Scope/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Dedoc\Scramble\Infer\Definition\ClassDefinition;
use Dedoc\Scramble\Infer\Services\FileNameResolver;
use Dedoc\Scramble\Infer\Services\ReferenceTypeResolver;
use Dedoc\Scramble\Infer\SimpleTypeGetters\BooleanNotTypeGetter;
use Dedoc\Scramble\Infer\SimpleTypeGetters\CastTypeGetter;
use Dedoc\Scramble\Infer\SimpleTypeGetters\ClassConstFetchTypeGetter;
Expand Down Expand Up @@ -305,6 +306,17 @@ public function addVariableType(int $line, string $name, Type $type)
$this->variables[$name][] = compact('line', 'type');
}

/**
* @deprecated
*/
public function getPropertyFetchType(Type $calledOn, string $propertyName): Type
{
return (new ReferenceTypeResolver($this->index))->resolve(
$this,
new PropertyFetchReferenceType($calledOn, $propertyName)
);
}

private function getVariableType(Node\Expr\Variable $node)
{
$name = (string) $node->name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public function toResponse(Type $type)

public function reference(ObjectType $type)
{
return new Reference('responses', Str::start($type->name, '\\'), $this->components);
return app(Reference::class, [
'referenceType' => 'responses',
'fullName' => Str::start($type->name, '\\'),
'components' => $this->components,
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public function toResponse(Type $type)

public function reference(ObjectType $type)
{
return new Reference('responses', Str::start($type->name, '\\'), $this->components);
return app(Reference::class, [
'referenceType' => 'responses',
'fullName' => Str::start($type->name, '\\'),
'components' => $this->components,
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public function toResponse(Type $type)

public function reference(ObjectType $type)
{
return new Reference('responses', Str::start($type->name, '\\'), $this->components);
return app(Reference::class, [
'referenceType' => 'responses',
'fullName' => Str::start($type->name, '\\'),
'components' => $this->components,
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public function toResponse(Type $type)

public function reference(ObjectType $type)
{
return new Reference('responses', Str::start($type->name, '\\'), $this->components);
return app(Reference::class, [
'referenceType' => 'responses',
'fullName' => Str::start($type->name, '\\'),
'components' => $this->components,
]);
}
}
Loading