Skip to content
Merged
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
19 changes: 0 additions & 19 deletions database/factories/ModelFactory.php

This file was deleted.

4 changes: 2 additions & 2 deletions resources/views/asyncapi.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
}, document.getElementById('asyncapi'));
})
.catch(err => {
console.error("Erro ao carregar o schema:", err);
document.getElementById('asyncapi').innerHTML = '<p style="color:red; padding:20px;">Erro ao carregar a documentação JSON.</p>';
console.error("Error loading schema:", err);
document.getElementById('asyncapi').innerHTML = '<p style="color:red; padding:20px;">Error loading the JSON documentation.</p>';
});
</script>
</body>
Expand Down
5 changes: 0 additions & 5 deletions src/AsyncApiServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ class AsyncApiServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
/*
* This class is a Package Service Provider
*
* More info: https://github.com/spatie/laravel-package-tools
*/
$package
->name('async-api')
->hasConfigFile('async-api')
Expand Down
12 changes: 8 additions & 4 deletions src/Attributes/AsyncApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Victormgomes\AsyncApi\Attributes;

use Attribute;
use Victormgomes\AsyncApi\Enums\Action;

#[Attribute(Attribute::TARGET_CLASS)]
class AsyncApi
Expand All @@ -14,16 +15,19 @@ public function __construct(
public ?string $dto = null,
public string $description = '',
public ?string $name = null,
// AsyncAPI 3.0 Standard Properties
public ?string $summary = null,
public ?string $operationId = null,
public string $action = 'send', // 'send' or 'receive'
public Action $action = Action::Send,
/** @var string[] */
public array $tags = [],
/** @var array<mixed> */
public array $examples = [],
/** @var array<string, mixed> */
public array $bindings = [],
/** @var array{url: string, description?: string}|null */
public ?array $externalDocs = null,
// Advanced 3.0 Properties
public ?string $correlationId = null,
public ?array $security = null, // Custom security for this specific operation
/** @var array<string|array<string, mixed>>|null $security Custom security schemes for this specific operation */
public ?array $security = null,
) {}
}
13 changes: 4 additions & 9 deletions src/Commands/AsyncApiCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,16 @@

use Illuminate\Console\Command;
use Victormgomes\AsyncApi\Services\Docs\AsyncApiGenerator;
use Victormgomes\AsyncApi\Services\Docs\SchemaConverter;

class AsyncApiCommand extends Command
{
protected $signature = 'docs:asyncapi';

protected $description = 'Gera a documentação AsyncAPI automaticamente.';
protected $description = 'Generate AsyncAPI documentation automatically.';

public function handle(): int
public function handle(AsyncApiGenerator $generator): int
{
$this->info('🚀 Iniciando escaneamento de eventos...');

// Injeção de dependência manual (ou via container)
$converter = new SchemaConverter;
$generator = new AsyncApiGenerator($converter);
$this->info('🚀 Starting event scan...');

$docs = $generator->generate();

Expand All @@ -31,7 +26,7 @@ public function handle(): int

file_put_contents($path, json_encode($docs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

$this->info('✅ Arquivo gerado com sucesso!');
$this->info('✅ File generated successfully!');
$this->info('📂 '.$path);

return self::SUCCESS;
Expand Down
11 changes: 11 additions & 0 deletions src/Enums/Action.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Victormgomes\AsyncApi\Enums;

enum Action: string
{
case Send = 'send';
case Receive = 'receive';
}
87 changes: 48 additions & 39 deletions src/Services/Docs/AsyncApiGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function __construct(

public function generate(): array
{
$this->log('🚀 INICIANDO GERAÇÃO (GLOBAL CONFIG MODE)...');
$this->log('🚀 STARTING GENERATION (GLOBAL CONFIG MODE)...');

$info = [
'title' => config('async-api.info_title', config('app.name').' Broadcasting API'),
Expand Down Expand Up @@ -85,7 +85,7 @@ public function generate(): array

$paths = array_filter([app_path(), base_path('Modules'), base_path('modules')], 'is_dir');

$this->log('🔍 Varrendo a aplicação com Ranger nos diretórios: '.implode(', ', $paths));
$this->log('🔍 Scanning application with Ranger in directories: '.implode(', ', $paths));

$ranger = app(Ranger::class);
$ranger->setAppPaths(...$paths);
Expand All @@ -94,7 +94,7 @@ public function generate(): array

$validClassesCount = 0;

$ranger->onBroadcastEvent(function (BroadcastEvent $event) use (&$structure, $analyzer, &$validClassesCount) {
$ranger->onBroadcastEvent(function (BroadcastEvent $event) use (&$structure, $analyzer, &$validClassesCount): void {
$this->log("💎 EVENTO BROADCAST ENCONTRADO (Ranger): {$event->className}");
$processed = $this->processEvent($event, $analyzer, $structure);
if ($processed) {
Expand All @@ -104,9 +104,8 @@ public function generate(): array

$ranger->walk();

$this->log('✅ SCAN FINALIZADO. Classes válidas processadas: '.$validClassesCount);
$this->log('✅ SCAN FINISHED. Valid classes processed: '.$validClassesCount);

// Adiciona schemas reutilizáveis coletados durante o processamento
$structure['components']['schemas'] = $this->schemaConverter->getSchemas();

if (empty($structure['channels'])) {
Expand Down Expand Up @@ -138,48 +137,35 @@ private function processEvent(BroadcastEvent $event, Analyzer $analyzer, array &
try {
$reflection = new ReflectionClass($className);

// Permite ignorar o evento se tiver o atributo AsyncApiIgnore
if (! empty($reflection->getAttributes(AsyncApiIgnore::class))) {
$this->log(" 🚫 Ignorado por AsyncApiIgnore: $className");
$this->log(" 🚫 Ignored by AsyncApiIgnore: $className");

return false;
}

$attributes = $reflection->getAttributes(AsyncApi::class);
$attr = ! empty($attributes) ? $attributes[0]->newInstance() : new AsyncApi;

$this->log("📝 Processando Classe: $className");
$this->log("📝 Processing class: $className");

$channelUri = $attr->channel;
if (! $channelUri) {
$channelUri = $this->inferChannelFromSurveyor($className, $analyzer);
}

if (! $channelUri) {
$this->log(' ❌ ERRO: Não foi possível ler o canal automaticamente.');
$this->log(' ❌ ERROR: Could not read the channel automatically.');

return false;
}

// Normaliza as variáveis PHP injetadas no channel URI para o formato AsyncAPI {param}
$channelUri = preg_replace_callback('/\{\$(?:this->)?(?:[a-zA-Z0-9_]+->)*([a-zA-Z0-9_]+)\}/', function ($m) {
return '{'.$m[1].'}';
}, $channelUri);
$channelUri = $this->normalizePhpVarsToAsyncApiParams($channelUri);

$channelUri = preg_replace_callback('/\$(?:this->)?(?:[a-zA-Z0-9_]+->)*([a-zA-Z0-9_]+)/', function ($m) {
return '{'.$m[1].'}';
}, $channelUri);

// Usa o nome da mensagem extraído nativamente pelo Surveyor/Ranger via $event->name
$eventName = $attr->name ?? $event->name;

// Delega a geração do schema inteiramente ao Surveyor Type
$payloadSchema = $this->schemaConverter->convertSurveyorType($event->data);

// Envolve o schema caso o Surveyor retorne apenas propriedades isoladas de objeto e não o object root
if (isset($payloadSchema['properties']) && ! isset($payloadSchema['type'])) {
$payloadSchema['type'] = 'object';
}
$payloadSchema = $this->ensureSchemaHasObjectType($payloadSchema);

$channelKey = str_replace(['{', '}', '.', '/'], '_', $channelUri);
if (! isset($structure['channels'][$channelKey])) {
Expand All @@ -192,7 +178,7 @@ private function processEvent(BroadcastEvent $event, Analyzer $analyzer, array &
$parameters = [];
foreach ($matches[1] as $paramName) {
$parameters[$paramName] = [
'description' => "Parâmetro dinâmico: $paramName",
'description' => "Dynamic parameter: $paramName",
];
}
$structure['channels'][$channelKey]['parameters'] = $parameters;
Expand Down Expand Up @@ -227,9 +213,9 @@ private function processEvent(BroadcastEvent $event, Analyzer $analyzer, array &
}
}

$operationId = $attr->operationId ?? $attr->action.$eventName;
$operationId = $attr->operationId ?? $attr->action->value.$eventName;
$structure['operations'][$operationId] = array_filter([
'action' => $attr->action,
'action' => $attr->action->value,
'channel' => ['$ref' => "#/channels/$channelKey"],
'summary' => $attr->summary ?? "Operation for $eventName",
'security' => $security,
Expand All @@ -238,12 +224,12 @@ private function processEvent(BroadcastEvent $event, Analyzer $analyzer, array &
],
]);

$this->log(" ✨ Sucesso! Adicionado ao canal: $channelUri com schema dinâmico.");
$this->log(" ✨ Success! Added to channel: $channelUri with dynamic schema.");

return true;

} catch (Throwable $e) {
$this->log(" 💀 EXCEPTION em $className: ".$e->getMessage());
$this->log(" 💀 EXCEPTION in $className: ".$e->getMessage());

return false;
}
Expand All @@ -261,7 +247,7 @@ private function inferChannelFromSurveyor(string $className, Analyzer $analyzer)

return $this->extractChannelUriFromType($returnType);
} catch (Throwable $e) {
$this->log(' 💀 ERRO AO INFERIR CANAL via Surveyor: '.$e->getMessage());
$this->log(' 💀 ERROR INFERRING CHANNEL via Surveyor: '.$e->getMessage());

return null;
}
Expand All @@ -278,16 +264,39 @@ private function extractChannelUriFromType(mixed $type): ?string
}
}

if ($type instanceof ArrayType) {
if (! empty($type->value)) {
// Para manter a compatibilidade original que esperava uma string de URI,
// retornamos a primeira URI válida encontrada no array.
foreach ($type->value as $item) {
$channel = $this->extractChannelUriFromType($item);
if ($channel) {
return $channel;
}
}
if ($type instanceof ArrayType && ! empty($type->value)) {
return $this->findFirstValidChannelUri($type->value);
}

return null;
}

private function normalizePhpVarsToAsyncApiParams(string $channelUri): string
{
$channelUri = preg_replace_callback('/\{\$(?:this->)?(?:[a-zA-Z0-9_]+->)*([a-zA-Z0-9_]+)\}/', function ($m) {
return '{'.$m[1].'}';
}, $channelUri);

return preg_replace_callback('/\$(?:this->)?(?:[a-zA-Z0-9_]+->)*([a-zA-Z0-9_]+)/', function ($m) {
return '{'.$m[1].'}';
}, $channelUri);
}

private function ensureSchemaHasObjectType(array $payloadSchema): array
{
if (isset($payloadSchema['properties']) && ! isset($payloadSchema['type'])) {
$payloadSchema['type'] = 'object';
}

return $payloadSchema;
}

private function findFirstValidChannelUri(array $value): ?string
{
foreach ($value as $item) {
$channel = $this->extractChannelUriFromType($item);
if ($channel) {
return $channel;
}
}

Expand Down
36 changes: 18 additions & 18 deletions src/Services/Docs/SchemaConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ class SchemaConverter
{
private array $schemas = [];

/**
* Converte um tipo nativo do Surveyor em um JSON Schema válido.
*/
public function convertSurveyorType(SurveyorType $type): array
{
if ($type instanceof ClassType) {
Expand All @@ -50,7 +47,7 @@ public function convertSurveyorType(SurveyorType $type): array
];
}

return $this->convert($type->value, true);
return $this->convertDtoClassToJsonSchema($type->value, true);
}

if ($type instanceof ArrayType) {
Expand Down Expand Up @@ -89,14 +86,12 @@ public function convertSurveyorType(SurveyorType $type): array
$oneOf = [];
foreach ($type->types as $subType) {
if ($subType instanceof NullType) {
continue; // Lidamos com null separadamente
continue;
}
$oneOf[] = $this->convertSurveyorType($subType);
}
if (count($oneOf) === 1) {
$schema = $oneOf[0];

return $schema; // Em OpenAPI 3.0 não tem nullable fácil, vamos simplificar o schema
return $this->simplifyNullableToOneOfSchema($oneOf);
}
if (count($oneOf) > 1) {
return ['oneOf' => $oneOf];
Expand All @@ -119,10 +114,7 @@ public function convertSurveyorType(SurveyorType $type): array
return ['type' => 'string'];
}

/**
* Converte uma classe DTO em um array de propriedades JSON Schema.
*/
public function convert(string $className, bool $asRef = false): array
public function convertDtoClassToJsonSchema(string $className, bool $asRef = false): array
{
if (! class_exists($className)) {
return [];
Expand All @@ -131,21 +123,29 @@ public function convert(string $className, bool $asRef = false): array
$shortName = (new ReflectionClass($className))->getShortName();

if ($asRef) {
if (! isset($this->schemas[$shortName])) {
// Importante: extrair propriedades ANTES de registrar para evitar recursão infinita parcial
$this->schemas[$shortName] = ['type' => 'object']; // Placeholder
$this->schemas[$shortName] = $this->extractProperties($className);
}
$this->registerSchemaWithCycleGuard($className, $shortName);

return ['$ref' => "#/components/schemas/$shortName"];
}

return $this->extractProperties($className);
}

private function registerSchemaWithCycleGuard(string $className, string $shortName): void
{
if (! isset($this->schemas[$shortName])) {
$this->schemas[$shortName] = ['type' => 'object'];
$this->schemas[$shortName] = $this->extractProperties($className);
}
}

private function simplifyNullableToOneOfSchema(array $oneOf): array
{
return $oneOf[0];
}

private function extractProperties(string $className): array
{
// Agora usamos o Analyzer do Surveyor para extrair as propriedades em vez do Reflection nativo
$analyzer = app(Analyzer::class);
$analyzed = $analyzer->analyzeClass($className)->result();

Expand Down
Loading