diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php deleted file mode 100644 index 2abe971..0000000 --- a/database/factories/ModelFactory.php +++ /dev/null @@ -1,19 +0,0 @@ - { - console.error("Erro ao carregar o schema:", err); - document.getElementById('asyncapi').innerHTML = '

Erro ao carregar a documentação JSON.

'; + console.error("Error loading schema:", err); + document.getElementById('asyncapi').innerHTML = '

Error loading the JSON documentation.

'; }); diff --git a/src/AsyncApiServiceProvider.php b/src/AsyncApiServiceProvider.php index d5a2a35..92ae892 100644 --- a/src/AsyncApiServiceProvider.php +++ b/src/AsyncApiServiceProvider.php @@ -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') diff --git a/src/Attributes/AsyncApi.php b/src/Attributes/AsyncApi.php index fa40547..4b9dd32 100644 --- a/src/Attributes/AsyncApi.php +++ b/src/Attributes/AsyncApi.php @@ -5,6 +5,7 @@ namespace Victormgomes\AsyncApi\Attributes; use Attribute; +use Victormgomes\AsyncApi\Enums\Action; #[Attribute(Attribute::TARGET_CLASS)] class AsyncApi @@ -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 */ public array $examples = [], + /** @var array */ 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>|null $security Custom security schemes for this specific operation */ + public ?array $security = null, ) {} } diff --git a/src/Commands/AsyncApiCommand.php b/src/Commands/AsyncApiCommand.php index 323c040..97858c0 100644 --- a/src/Commands/AsyncApiCommand.php +++ b/src/Commands/AsyncApiCommand.php @@ -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(); @@ -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; diff --git a/src/Enums/Action.php b/src/Enums/Action.php new file mode 100644 index 0000000..5494cad --- /dev/null +++ b/src/Enums/Action.php @@ -0,0 +1,11 @@ +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'), @@ -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); @@ -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) { @@ -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'])) { @@ -138,9 +137,8 @@ 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; } @@ -148,7 +146,7 @@ private function processEvent(BroadcastEvent $event, Analyzer $analyzer, array & $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) { @@ -156,30 +154,18 @@ private function processEvent(BroadcastEvent $event, Analyzer $analyzer, array & } 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])) { @@ -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; @@ -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, @@ -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; } @@ -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; } @@ -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; } } diff --git a/src/Services/Docs/SchemaConverter.php b/src/Services/Docs/SchemaConverter.php index a25215c..c7c67ad 100644 --- a/src/Services/Docs/SchemaConverter.php +++ b/src/Services/Docs/SchemaConverter.php @@ -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) { @@ -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) { @@ -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]; @@ -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 []; @@ -131,11 +123,7 @@ 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"]; } @@ -143,9 +131,21 @@ public function convert(string $className, bool $asRef = false): array 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(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 3edf5c6..7fa40d0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -27,11 +27,5 @@ protected function getPackageProviders($app) public function getEnvironmentSetUp($app) { config()->set('database.default', 'testing'); - - /* - foreach (\Illuminate\Support\Facades\File::allFiles(__DIR__ . '/../database/migrations') as $migration) { - (include $migration->getRealPath())->up(); - } - */ } }