From 36af4eb241faf6f20fc0c2e29d14f9bf811c9337 Mon Sep 17 00:00:00 2001 From: Jasper Tey Date: Sat, 16 Nov 2024 18:31:14 -0500 Subject: [PATCH] Tighten up the generator blueprint and add more test coverage. --- src/Commands/Concerns/CanPromptForDomain.php | 35 ----- .../Concerns/ResolvesDomainFromInput.php | 34 ++++- src/Support/GeneratorBlueprint.php | 43 ++++-- tests/Datasets/GeneratorSchemas.php | 27 ++++ tests/Support/BlueprintTest.php | 123 ++++++++++++++++++ tests/Support/DomainTest.php | 2 + 6 files changed, 212 insertions(+), 52 deletions(-) delete mode 100644 src/Commands/Concerns/CanPromptForDomain.php create mode 100644 tests/Datasets/GeneratorSchemas.php create mode 100644 tests/Support/BlueprintTest.php diff --git a/src/Commands/Concerns/CanPromptForDomain.php b/src/Commands/Concerns/CanPromptForDomain.php deleted file mode 100644 index f4d95e9..0000000 --- a/src/Commands/Concerns/CanPromptForDomain.php +++ /dev/null @@ -1,35 +0,0 @@ -mapWithKeys(fn ($name) => [Str::lower($name) => $name]); - - // Prompt for the domain - $domainName = suggest( - label: 'What is the domain?', - options: fn ($value) => collect($choices) - ->filter(fn ($name) => Str::contains($name, $value, ignoreCase: true)) - ->toArray(), - placeholder: 'Start typing to search...', - required: true - ); - - // Normalize the case of the domain name - // if it is an existing domain. - if ($match = $choices->get(Str::lower($domainName))) { - $domainName = $match; - } - - return $domainName; - } -} diff --git a/src/Commands/Concerns/ResolvesDomainFromInput.php b/src/Commands/Concerns/ResolvesDomainFromInput.php index 103dd77..efe35a0 100644 --- a/src/Commands/Concerns/ResolvesDomainFromInput.php +++ b/src/Commands/Concerns/ResolvesDomainFromInput.php @@ -4,13 +4,15 @@ use Illuminate\Support\Str; use Lunarstorm\LaravelDDD\Support\Domain; +use Lunarstorm\LaravelDDD\Support\DomainResolver; use Lunarstorm\LaravelDDD\Support\GeneratorBlueprint; use Symfony\Component\Console\Input\InputOption; +use function Laravel\Prompts\suggest; + trait ResolvesDomainFromInput { - use CanPromptForDomain, - HandleHooks, + use HandleHooks, HasGeneratorBlueprint, QualifiesDomainModels; @@ -48,6 +50,30 @@ protected function qualifyClass($name) return $this->blueprint->qualifyClass($name); } + protected function promptForDomainName(): string + { + $choices = collect(DomainResolver::domainChoices()) + ->mapWithKeys(fn ($name) => [Str::lower($name) => $name]); + + // Prompt for the domain + $domainName = suggest( + label: 'What is the domain?', + options: fn ($value) => collect($choices) + ->filter(fn ($name) => Str::contains($name, $value, ignoreCase: true)) + ->toArray(), + placeholder: 'Start typing to search...', + required: true + ); + + // Normalize the case of the domain name + // if it is an existing domain. + if ($match = $choices->get(Str::lower($domainName))) { + $domainName = $match; + } + + return $domainName; + } + protected function beforeHandle() { $nameInput = $this->getNameInput(); @@ -72,9 +98,11 @@ protected function beforeHandle() }; $this->blueprint = new GeneratorBlueprint( + commandName: $this->getName(), nameInput: $nameInput, domainName: $domainName, - command: $this, + arguments: $this->arguments(), + options: $this->options(), ); $this->input->setArgument('name', $this->blueprint->nameInput); diff --git a/src/Support/GeneratorBlueprint.php b/src/Support/GeneratorBlueprint.php index 5a371de..00e1354 100644 --- a/src/Support/GeneratorBlueprint.php +++ b/src/Support/GeneratorBlueprint.php @@ -11,6 +11,10 @@ class GeneratorBlueprint { public string $nameInput; + public string $normalizedName; + + public string $baseName; + public string $domainName; public ?Domain $domain = null; @@ -26,27 +30,42 @@ class GeneratorBlueprint public string $type; public function __construct( + string $commandName, string $nameInput, string $domainName, - Command $command, + array $arguments = [], + array $options = [], ) { - $this->nameInput = str($nameInput)->studly()->replace(['.', '\\', '/'], '/')->toString(); - - $this->domain = new Domain($domainName); + $this->command = new CommandContext($commandName, $arguments, $options); - $this->domainName = $this->domain->domainWithSubdomain; - - $this->command = new CommandContext($command->getName(), $command->arguments(), $command->options()); + $this->nameInput = str($nameInput)->toString(); $this->isAbsoluteName = str($this->nameInput)->startsWith('/'); $this->type = $this->guessObjectType(); + $this->normalizedName = Path::normalizeNamespace( + str($nameInput) + ->studly() + ->replace(['.', '\\', '/'], '\\') + ->trim('\\') + ->when($this->type === 'factory', fn ($name) => $name->finish('Factory')) + ->toString() + ); + + $this->baseName = class_basename($this->normalizedName); + + $this->domain = new Domain($domainName); + + $this->domainName = $this->domain->domainWithSubdomain; + $this->layer = DomainResolver::resolveLayer($this->domainName, $this->type); $this->schema = $this->resolveSchema(); } + public static function capture(Command $command) {} + protected function guessObjectType(): string { return match ($this->command->name) { @@ -82,16 +101,12 @@ protected function resolveSchema(): ObjectSchema default => $this->layer->namespaceFor($this->type), }; - $baseName = str($this->nameInput) - ->replace(['\\', '/'], '\\') - ->trim('\\') - ->when($this->type === 'factory', fn ($name) => $name->finish('Factory')) + $fullyQualifiedName = str($this->normalizedName) + ->start($namespace.'\\') ->toString(); - $fullyQualifiedName = $namespace.'\\'.$baseName; - return new ObjectSchema( - name: $this->nameInput, + name: $this->normalizedName, namespace: $namespace, fullyQualifiedName: $fullyQualifiedName, path: $this->layer->path($fullyQualifiedName), diff --git a/tests/Datasets/GeneratorSchemas.php b/tests/Datasets/GeneratorSchemas.php new file mode 100644 index 0000000..c899f7e --- /dev/null +++ b/tests/Datasets/GeneratorSchemas.php @@ -0,0 +1,27 @@ + [ + 'ddd:model', + 'Invoice', + 'Invoicing', + [ + 'name' => 'Invoice', + 'namespace' => 'Domain\Invoicing\Models', + 'fullyQualifiedName' => 'Domain\Invoicing\Models\Invoice', + 'path' => 'src/Domain/Invoicing/Models/Invoice.php', + ], + ], + + 'ddd:model Invoicing:Invoice' => [ + 'ddd:model', + 'InvoicingEntry', + 'Invoicing', + [ + 'name' => 'Invoice', + 'namespace' => 'Domain\Invoicing\Models', + 'fullyQualifiedName' => 'Domain\Invoicing\Models\Invoice', + 'path' => 'src/Domain/Invoicing/Models/Invoice.php', + ], + ], +]); diff --git a/tests/Support/BlueprintTest.php b/tests/Support/BlueprintTest.php new file mode 100644 index 0000000..a633b79 --- /dev/null +++ b/tests/Support/BlueprintTest.php @@ -0,0 +1,123 @@ +set([ + 'ddd.domain_path' => 'src/Domain', + 'ddd.domain_namespace' => 'Domain', + 'ddd.application_namespace' => 'Application', + 'ddd.application_path' => 'src/Application', + 'ddd.application_objects' => [ + 'controller', + 'request', + 'middleware', + ], + 'ddd.layers' => [ + 'Infrastructure' => 'src/Infrastructure', + 'NestedLayer' => 'src/Nested/Layer', + 'AppNested' => 'app/Nested', + ], + ]); +}); + +it('handles nested objects', function ($nameInput, $normalized) { + $blueprint = new GeneratorBlueprint( + commandName: 'ddd:model', + nameInput: $nameInput, + domainName: 'SomeDomain', + ); + + expect($blueprint->schema) + ->name->toBe($normalized) + ->namespace->toBe('Domain\SomeDomain\Models'); +})->with([ + ['Nested\\Thing', 'Nested\\Thing'], + ['Nested/Thing', 'Nested\\Thing'], + ['Nested/Thing/Deeply', 'Nested\\Thing\\Deeply'], + ['Nested\\Thing/Deeply', 'Nested\\Thing\\Deeply'], +]); + +it('handles objects in the application layer', function ($command, $domainName, $nameInput, $expectedName, $expectedNamespace, $expectedFqn, $expectedPath) { + $blueprint = new GeneratorBlueprint( + commandName: $command, + nameInput: $nameInput, + domainName: $domainName, + ); + + expect($blueprint->schema) + ->name->toBe($expectedName) + ->namespace->toBe($expectedNamespace) + ->fullyQualifiedName->toBe($expectedFqn) + ->path->toBe($expectedPath); +})->with([ + ['ddd:controller', 'SomeDomain', 'ApplicationController', 'ApplicationController', 'Application\\SomeDomain\\Controllers', 'Application\\SomeDomain\\Controllers\\ApplicationController', 'src/Application/SomeDomain/Controllers/ApplicationController.php'], + ['ddd:controller', 'SomeDomain', 'Application', 'Application', 'Application\\SomeDomain\\Controllers', 'Application\\SomeDomain\\Controllers\\Application', 'src/Application/SomeDomain/Controllers/Application.php'], + ['ddd:middleware', 'SomeDomain', 'CrazyMiddleware', 'CrazyMiddleware', 'Application\\SomeDomain\\Middleware', 'Application\\SomeDomain\\Middleware\\CrazyMiddleware', 'src/Application/SomeDomain/Middleware/CrazyMiddleware.php'], + ['ddd:request', 'SomeDomain', 'LazyRequest', 'LazyRequest', 'Application\\SomeDomain\\Requests', 'Application\\SomeDomain\\Requests\\LazyRequest', 'src/Application/SomeDomain/Requests/LazyRequest.php'], +]); + +it('handles objects in custom layers', function ($command, $domainName, $nameInput, $expectedName, $expectedNamespace, $expectedFqn, $expectedPath) { + $blueprint = new GeneratorBlueprint( + commandName: $command, + nameInput: $nameInput, + domainName: $domainName, + ); + + expect($blueprint->schema) + ->name->toBe($expectedName) + ->namespace->toBe($expectedNamespace) + ->fullyQualifiedName->toBe($expectedFqn) + ->path->toBe($expectedPath); +})->with([ + ['ddd:model', 'Infrastructure', 'System', 'System', 'Infrastructure\\Models', 'Infrastructure\\Models\\System', 'src/Infrastructure/Models/System.php'], + ['ddd:factory', 'Infrastructure', 'System', 'SystemFactory', 'Infrastructure\\Database\\Factories', 'Infrastructure\\Database\\Factories\\SystemFactory', 'src/Infrastructure/Database/Factories/SystemFactory.php'], + ['ddd:provider', 'Infrastructure', 'InfrastructureServiceProvider', 'InfrastructureServiceProvider', 'Infrastructure\\Providers', 'Infrastructure\\Providers\\InfrastructureServiceProvider', 'src/Infrastructure/Providers/InfrastructureServiceProvider.php'], + ['ddd:provider', 'Infrastructure', 'Infrastructure\\InfrastructureServiceProvider', 'Infrastructure\\InfrastructureServiceProvider', 'Infrastructure\\Providers', 'Infrastructure\\Providers\\Infrastructure\\InfrastructureServiceProvider', 'src/Infrastructure/Providers/Infrastructure/InfrastructureServiceProvider.php'], + ['ddd:provider', 'Infrastructure', 'InfrastructureServiceProvider', 'InfrastructureServiceProvider', 'Infrastructure\\Providers', 'Infrastructure\\Providers\\InfrastructureServiceProvider', 'src/Infrastructure/Providers/InfrastructureServiceProvider.php'], + ['ddd:provider', 'AppNested', 'CrazyServiceProvider', 'CrazyServiceProvider', 'AppNested\\Providers', 'AppNested\\Providers\\CrazyServiceProvider', 'app/Nested/Providers/CrazyServiceProvider.php'], + ['ddd:provider', 'NestedLayer', 'CrazyServiceProvider', 'CrazyServiceProvider', 'NestedLayer\\Providers', 'NestedLayer\\Providers\\CrazyServiceProvider', 'src/Nested/Layer/Providers/CrazyServiceProvider.php'], +]); + +it('handles objects whose name contains the domain name', function ($command, $domainName, $nameInput, $expectedName, $expectedNamespace, $expectedFqn, $expectedPath) { + $blueprint = new GeneratorBlueprint( + commandName: $command, + nameInput: $nameInput, + domainName: $domainName, + ); + + expect($blueprint->schema) + ->name->toBe($expectedName) + ->namespace->toBe($expectedNamespace) + ->fullyQualifiedName->toBe($expectedFqn) + ->path->toBe($expectedPath); +})->with([ + ['ddd:model', 'SomeDomain', 'SomeDomain', 'SomeDomain', 'Domain\\SomeDomain\\Models', 'Domain\\SomeDomain\\Models\\SomeDomain', 'src/Domain/SomeDomain/Models/SomeDomain.php'], + ['ddd:model', 'SomeDomain', 'SomeDomainModel', 'SomeDomainModel', 'Domain\\SomeDomain\\Models', 'Domain\\SomeDomain\\Models\\SomeDomainModel', 'src/Domain/SomeDomain/Models/SomeDomainModel.php'], + ['ddd:model', 'SomeDomain', 'Nested\\SomeDomain', 'Nested\\SomeDomain', 'Domain\\SomeDomain\\Models', 'Domain\\SomeDomain\\Models\\Nested\\SomeDomain', 'src/Domain/SomeDomain/Models/Nested/SomeDomain.php'], + ['ddd:model', 'SomeDomain', 'SomeDomain\\SomeDomain', 'SomeDomain\\SomeDomain', 'Domain\\SomeDomain\\Models', 'Domain\\SomeDomain\\Models\\SomeDomain\\SomeDomain', 'src/Domain/SomeDomain/Models/SomeDomain/SomeDomain.php'], + ['ddd:model', 'SomeDomain', 'SomeDomain\\SomeDomainModel', 'SomeDomain\\SomeDomainModel', 'Domain\\SomeDomain\\Models', 'Domain\\SomeDomain\\Models\\SomeDomain\\SomeDomainModel', 'src/Domain/SomeDomain/Models/SomeDomain/SomeDomainModel.php'], + ['ddd:model', 'Infrastructure', 'Infrastructure', 'Infrastructure', 'Infrastructure\\Models', 'Infrastructure\\Models\\Infrastructure', 'src/Infrastructure/Models/Infrastructure.php'], + ['ddd:model', 'Infrastructure', 'Nested\\Infrastructure', 'Nested\\Infrastructure', 'Infrastructure\\Models', 'Infrastructure\\Models\\Nested\\Infrastructure', 'src/Infrastructure/Models/Nested/Infrastructure.php'], + ['ddd:controller', 'SomeDomain', 'SomeDomain', 'SomeDomain', 'Application\\SomeDomain\\Controllers', 'Application\\SomeDomain\\Controllers\\SomeDomain', 'src/Application/SomeDomain/Controllers/SomeDomain.php'], + ['ddd:controller', 'SomeDomain', 'SomeDomainController', 'SomeDomainController', 'Application\\SomeDomain\\Controllers', 'Application\\SomeDomain\\Controllers\\SomeDomainController', 'src/Application/SomeDomain/Controllers/SomeDomainController.php'], + ['ddd:controller', 'SomeDomain', 'SomeDomain\\SomeDomain', 'SomeDomain\\SomeDomain', 'Application\\SomeDomain\\Controllers', 'Application\\SomeDomain\\Controllers\\SomeDomain\\SomeDomain', 'src/Application/SomeDomain/Controllers/SomeDomain/SomeDomain.php'], +]); + +it('handles absolute-path names', function ($command, $domainName, $nameInput, $expectedName, $expectedNamespace, $expectedFqn, $expectedPath) { + $blueprint = new GeneratorBlueprint( + commandName: $command, + nameInput: $nameInput, + domainName: $domainName, + ); + + expect($blueprint->schema) + ->name->toBe($expectedName) + ->namespace->toBe($expectedNamespace) + ->fullyQualifiedName->toBe($expectedFqn) + ->path->toBe($expectedPath); +})->with([ + ['ddd:model', 'SomeDomain', '/RootModel', 'RootModel', 'Domain\\SomeDomain', 'Domain\\SomeDomain\\RootModel', 'src/Domain/SomeDomain/RootModel.php'], + ['ddd:model', 'SomeDomain', '/CustomLocation/Thing', 'CustomLocation\\Thing', 'Domain\\SomeDomain', 'Domain\\SomeDomain\\CustomLocation\\Thing', 'src/Domain/SomeDomain/CustomLocation/Thing.php'], + ['ddd:model', 'SomeDomain', '/Custom/Nested/Thing', 'Custom\\Nested\\Thing', 'Domain\\SomeDomain', 'Domain\\SomeDomain\\Custom\\Nested\\Thing', 'src/Domain/SomeDomain/Custom/Nested/Thing.php'], +]); diff --git a/tests/Support/DomainTest.php b/tests/Support/DomainTest.php index a88fbcf..accef75 100644 --- a/tests/Support/DomainTest.php +++ b/tests/Support/DomainTest.php @@ -28,6 +28,8 @@ ->path->toBe(Path::normalize($expectedPath)); })->with([ ['Reporting', 'InvoiceReport', 'Domain\\Reporting\\Models\\InvoiceReport', 'src/Domain/Reporting/Models/InvoiceReport.php'], + ['Reporting', 'ReportingLog', 'Domain\\Reporting\\Models\\ReportingLog', 'src/Domain/Reporting/Models/ReportingLog.php'], + ['Reporting', 'Reporting\Log', 'Domain\\Reporting\\Models\\Reporting\\Log', 'src/Domain/Reporting/Models/Reporting/Log.php'], ['Reporting.Internal', 'InvoiceReport', 'Domain\\Reporting\\Internal\\Models\\InvoiceReport', 'src/Domain/Reporting/Internal/Models/InvoiceReport.php'], ]);