Skip to content

Commit c1ec026

Browse files
authored
Merge pull request #95 from tarfin-labs/config-validator-command
Config validator command
2 parents 72421b1 + d763909 commit c1ec026

File tree

7 files changed

+286
-9
lines changed

7 files changed

+286
-9
lines changed

src/Console/Commands/GenerateUmlCommand.php renamed to src/Commands/GenerateUmlCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Tarfinlabs\EventMachine\Console\Commands;
5+
namespace Tarfinlabs\EventMachine\Commands;
66

77
use Illuminate\Console\Command;
88
use Illuminate\Support\Facades\File;
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tarfinlabs\EventMachine\Commands;
6+
7+
use Throwable;
8+
use ReflectionClass;
9+
use InvalidArgumentException;
10+
use Illuminate\Console\Command;
11+
use Illuminate\Support\Facades\File;
12+
use Symfony\Component\Finder\SplFileInfo;
13+
use Tarfinlabs\EventMachine\Actor\Machine;
14+
use Tarfinlabs\EventMachine\StateConfigValidator;
15+
16+
class MachineConfigValidatorCommand extends Command
17+
{
18+
/**
19+
* The name and signature of the console command.
20+
*
21+
* @var string
22+
*/
23+
protected $signature = 'machine:validate {machine?*} {--all : Validate all machines in the project}';
24+
25+
/**
26+
* The console command description.
27+
*
28+
* @var string
29+
*/
30+
protected $description = 'Validate machine configuration for potential issues';
31+
32+
/**
33+
* Execute the console command.
34+
*/
35+
public function handle(): void
36+
{
37+
if ($this->option(key: 'all')) {
38+
$this->validateAllMachines();
39+
40+
return;
41+
}
42+
43+
$machines = $this->argument(key: 'machine');
44+
if (empty($machines)) {
45+
$this->error(string: 'Please provide a machine class name or use --all option.');
46+
47+
return;
48+
}
49+
50+
foreach ($machines as $machine) {
51+
$this->validateMachine($machine);
52+
}
53+
}
54+
55+
/**
56+
* Validate a single machine configuration.
57+
*/
58+
protected function validateMachine(string $machineClass): void
59+
{
60+
try {
61+
// Find the full class name if short name is provided
62+
$fullClassName = $this->findMachineClass($machineClass);
63+
64+
if (!$fullClassName) {
65+
$this->error(string: "Machine class '{$machineClass}' not found.");
66+
67+
return;
68+
}
69+
70+
// Check if class exists and is a Machine
71+
if (!is_subclass_of(object_or_class: $fullClassName, class: Machine::class)) {
72+
$this->error(string: "Class '{$fullClassName}' is not a Machine.");
73+
74+
return;
75+
}
76+
77+
// Get machine definition and validate
78+
$definition = $fullClassName::definition();
79+
if ($definition === null) {
80+
$this->error(string: "Machine '{$fullClassName}' has no definition.");
81+
82+
return;
83+
}
84+
85+
StateConfigValidator::validate($definition->config);
86+
$this->info(string: "✓ Machine '{$fullClassName}' configuration is valid.");
87+
88+
} catch (InvalidArgumentException $e) {
89+
$this->error(string: "Configuration error in '{$fullClassName}':");
90+
$this->error(string: $e->getMessage());
91+
} catch (Throwable $e) {
92+
$this->error(string: "Error validating '{$machineClass}':");
93+
$this->error(string: $e->getMessage());
94+
}
95+
}
96+
97+
/**
98+
* Find machine class by name or FQN.
99+
*/
100+
protected function findMachineClass(string $class): ?string
101+
{
102+
// If it's already a FQN and exists, return it
103+
if (class_exists($class)) {
104+
return $class;
105+
}
106+
107+
// Get all potential machine classes
108+
$machineClasses = $this->findMachineClasses();
109+
110+
// First try exact match
111+
foreach ($machineClasses as $fqn) {
112+
if (str_ends_with($fqn, "\\{$class}")) {
113+
return $fqn;
114+
}
115+
}
116+
117+
// Then try case-insensitive match
118+
$lowercaseClass = strtolower($class);
119+
foreach ($machineClasses as $fqn) {
120+
if (str_ends_with(strtolower($fqn), "\\{$lowercaseClass}")) {
121+
return $fqn;
122+
}
123+
}
124+
125+
return null;
126+
}
127+
128+
/**
129+
* Find all potential machine classes in the project.
130+
*
131+
* @return array<string>
132+
*/
133+
protected function findMachineClasses(): array
134+
{
135+
$machineClasses = [];
136+
137+
// Get package path
138+
$packagePath = (new ReflectionClass(objectOrClass: Machine::class))->getFileName();
139+
$packagePath = dirname($packagePath, levels: 3); // Go up to package root
140+
141+
// Check tests directory
142+
$testsPath = $packagePath.'/tests';
143+
if (File::exists($testsPath)) {
144+
$this->findMachineClassesInDirectory(directory: $testsPath, machineClasses: $machineClasses);
145+
}
146+
147+
// Check src directory
148+
$srcPath = $packagePath.'/src';
149+
if (File::exists($srcPath)) {
150+
$this->findMachineClassesInDirectory(directory: $srcPath, machineClasses: $machineClasses);
151+
}
152+
153+
return array_values(array_unique($machineClasses));
154+
}
155+
156+
/**
157+
* Find machine classes in a specific directory.
158+
*
159+
* @param array<string> $machineClasses
160+
*/
161+
protected function findMachineClassesInDirectory(string $directory, array &$machineClasses): void
162+
{
163+
// Find all PHP files recursively
164+
$files = File::allFiles($directory);
165+
166+
foreach ($files as $file) {
167+
if (!$file instanceof SplFileInfo) {
168+
continue;
169+
}
170+
171+
// Get file contents
172+
$contents = File::get($file->getRealPath());
173+
174+
// Extract namespace
175+
preg_match(pattern: '/namespace\s+([^;]+)/i', subject: $contents, matches: $namespaceMatches);
176+
if (empty($namespaceMatches[1])) {
177+
continue;
178+
}
179+
$namespace = trim($namespaceMatches[1]);
180+
181+
// Extract class name
182+
preg_match(pattern: '/class\s+(\w+)/i', subject: $contents, matches: $classMatches);
183+
if (empty($classMatches[1])) {
184+
continue;
185+
}
186+
$className = trim($classMatches[1]);
187+
188+
// Create FQN
189+
$fqn = $namespace.'\\'.$className;
190+
191+
// Check if class exists and is a Machine
192+
if (class_exists($fqn) && is_subclass_of($fqn, Machine::class)) {
193+
$machineClasses[] = $fqn;
194+
}
195+
}
196+
}
197+
198+
/**
199+
* Validate all machines in the project.
200+
*/
201+
protected function validateAllMachines(): void
202+
{
203+
$validated = 0;
204+
$failed = 0;
205+
206+
$machineClasses = $this->findMachineClasses();
207+
208+
foreach ($machineClasses as $class) {
209+
try {
210+
$definition = $class::definition();
211+
if ($definition === null) {
212+
$this->warn(string: "Machine '{$class}' has no definition.");
213+
$failed++;
214+
215+
continue;
216+
}
217+
218+
StateConfigValidator::validate($definition->config);
219+
$this->info(string: "✓ Machine '{$class}' configuration is valid.");
220+
$validated++;
221+
} catch (Throwable $e) {
222+
$this->error(string: "✗ Error in '{$class}': ".$e->getMessage());
223+
$failed++;
224+
}
225+
}
226+
227+
$this->newLine();
228+
$this->info(string: "Validation complete: {$validated} valid, {$failed} failed");
229+
}
230+
}

src/MachineServiceProvider.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
use Spatie\LaravelPackageTools\Package;
88
use Spatie\LaravelPackageTools\PackageServiceProvider;
9-
use Tarfinlabs\EventMachine\Console\Commands\GenerateUmlCommand;
9+
use Tarfinlabs\EventMachine\Commands\GenerateUmlCommand;
10+
use Tarfinlabs\EventMachine\Commands\MachineConfigValidatorCommand;
1011

1112
/**
1213
* Class MachineServiceProvider.
@@ -33,6 +34,7 @@ public function configurePackage(Package $package): void
3334
->name('event-machine')
3435
->hasConfigFile('machine')
3536
->hasMigration('create_machine_events_table')
36-
->hasCommand(GenerateUmlCommand::class);
37+
->hasCommand(GenerateUmlCommand::class)
38+
->hasCommand(MachineConfigValidatorCommand::class);
3739
}
3840
}

tests/CommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
use Tarfinlabs\EventMachine\Console\Commands\GenerateUmlCommand;
5+
use Tarfinlabs\EventMachine\Commands\GenerateUmlCommand;
66

77
it('it has GenerateUmlCommand', function (): void {
88
$this->assertTrue(class_exists(GenerateUmlCommand::class));
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tarfinlabs\EventMachine\Tests\Commands;
6+
7+
use Tarfinlabs\EventMachine\Tests\TestCase;
8+
use Tarfinlabs\EventMachine\Tests\Stubs\Machines\AbcMachine;
9+
use Tarfinlabs\EventMachine\Tests\Stubs\Machines\Xyz\XyzMachine;
10+
11+
class MachineConfigValidatorCommandTest extends TestCase
12+
{
13+
public function test_it_validates_machine_with_valid_config(): void
14+
{
15+
$this
16+
->artisan('machine:validate', ['machine' => [class_basename(AbcMachine::class)]])
17+
->expectsOutput("✓ Machine '".AbcMachine::class."' configuration is valid.")
18+
->assertSuccessful();
19+
}
20+
21+
public function test_it_shows_error_for_non_existent_machine(): void
22+
{
23+
$this
24+
->artisan('machine:validate', ['machine' => ['NonExistentMachine']])
25+
->expectsOutput("Machine class 'NonExistentMachine' not found.")
26+
->assertSuccessful();
27+
}
28+
29+
public function test_it_validates_all_machines(): void
30+
{
31+
$this
32+
->artisan('machine:validate', ['--all' => true])
33+
->expectsOutput("✓ Machine '".AbcMachine::class."' configuration is valid.")
34+
->expectsOutput("✓ Machine '".XyzMachine::class."' configuration is valid.")
35+
->assertSuccessful();
36+
}
37+
38+
public function test_it_requires_machine_argument_or_all_option(): void
39+
{
40+
$this
41+
->artisan(command: 'machine:validate')
42+
->expectsOutput(output: 'Please provide a machine class name or use --all option.')
43+
->assertSuccessful();
44+
}
45+
}

tests/Pest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
|
3737
*/
3838

39-
//function something()
40-
//{
39+
// function something()
40+
// {
4141
// // ..
42-
//}
42+
// }

tests/StateDefinitionTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
expect($deepState->config)->toBe($machine->config['states']['one']['states']['deep']);
4848

4949
// TODO: Consider that if these should be reactive?
50-
//$deepState->config['meta'] = 'testing meta';
51-
//expect($machine->config['states']['one']['states']['deep']['meta'])->toBe('testing meta');
50+
// $deepState->config['meta'] = 'testing meta';
51+
// expect($machine->config['states']['one']['states']['deep']['meta'])->toBe('testing meta');
5252
});
5353

5454
test('a state definition has a key', function (): void {

0 commit comments

Comments
 (0)