diff --git a/composer.json b/composer.json index acdb6872..f378a372 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "justinrainbow/json-schema": "^6.0", "ext-json": "*", "composer/semver": "^3.0", - "laminas/laminas-code": "^4.12" + "laminas/laminas-code": "^4.12", + "nikic/php-parser": "^4.19" }, "autoload": { "psr-4": { diff --git a/src/Codegen/AbstractGenerator.php b/src/Codegen/AbstractGenerator.php new file mode 100644 index 00000000..cd7505bd --- /dev/null +++ b/src/Codegen/AbstractGenerator.php @@ -0,0 +1,116 @@ +nodes, $index, 0, [$node]); + + return $this; + } + + public function set(int $index, $value): self + { + if (!$this->isValidIndex($index)) { + throw new \OutOfRangeException(); + } + + $this->nodes[$index] = $value; + + return $this; + } + + public function remove(int $index): Node + { + if (!$this->isValidIndex($index)) { + throw new \OutOfRangeException(); + } + + return array_splice($this->nodes, $index, 1)[0]; + } + + public function clear(): self + { + $this->nodes = []; + + return $this; + } + + /** + * @psalm-param callable(Node): Node $callback + */ + public function walk(callable $callback): self + { + array_walk($this->nodes, $callback); + + return $this; + } + + /** + * @psalm-param ($mode is 0 ? callable(Node) : callable(Node, int) ) $callback + */ + public function filter(callable $callback, int $mode = 0): self + { + $this->nodes = array_filter($this->nodes, $callback, $mode); + $this->nodes = array_values($this->nodes); + + return $this; + } + + public function find(Node $value): ?int + { + $offset = array_search($value, $this->nodes, true); + + return $offset === false ? null : $offset; + } + + public function first(): Node + { + if (empty($this->nodes)) { + throw new \UnderflowException(); + } + + return $this->nodes[0]; + } + + public function get(int $index): Node + { + if (!$this->isValidIndex($index)) { + throw new \OutOfRangeException(); + } + + return $this->nodes[$index]; + } + + public function last(): Node + { + if (empty($this->nodes)) { + throw new \UnderflowException(); + } + + return $this->nodes[array_key_last($this->nodes)]; + } + + public function count(): int + { + return count($this->nodes); + } + + protected function isValidIndex(int $index): bool + { + return array_key_exists($index, $this->nodes); + } +} diff --git a/src/Codegen/EnumGenerator.php b/src/Codegen/EnumGenerator.php new file mode 100644 index 00000000..914aaf5c --- /dev/null +++ b/src/Codegen/EnumGenerator.php @@ -0,0 +1,82 @@ + */ + protected array $cases = []; + + public function __construct( + protected Enum_ $enum_, + protected Namespace_ $namespace_ + ) + { + parent::__construct([ + $this->buildStrictTypes(), + new Nop(), + $this->namespace_, + $this->enum_, + ]); + } + + public function withEnum_(Enum_ $enum_): self + { + foreach ($this->cases as $name => $case) { + $enum_->stmts[$name] = $case; + } + + $currentEnumIndex = $this->find($this->enum_); + if ($currentEnumIndex !== null) { + $this->set($currentEnumIndex, $enum_); + } + + $this->enum_ = $enum_; + + return $this; + } + + public function withNamespace_(Namespace_ $namespace_): self + { + $currentNamespaceIndex = $this->find($this->namespace_); + if ($currentNamespaceIndex !== null) { + $this->set($currentNamespaceIndex, $namespace_); + } + + $this->namespace_ = $namespace_; + + return $this; + } + + public function withAdditionalEnumCase(EnumCase $enumCase): self + { + $this->cases[$enumCase->name->toLowerString()] = $enumCase; + $this->enum_->stmts[$enumCase->name->toString()] = $enumCase; + + return $this; + } + + protected function buildStrictTypes(): Declare_ + { + $declareDeclare = new DeclareDeclare(new Identifier('strict_types'), new LNumber(1)); + return new Declare_([$declareDeclare]); + } + + public function generate(): string + { + $prettyPrinter = new Standard(); + return $prettyPrinter->prettyPrint($this->nodes); + } +} diff --git a/src/Generator/GeneratorHookRunner.php b/src/Generator/GeneratorHookRunner.php index 0c4309a3..6185d0a0 100644 --- a/src/Generator/GeneratorHookRunner.php +++ b/src/Generator/GeneratorHookRunner.php @@ -2,8 +2,8 @@ namespace Helmich\Schema2Class\Generator; +use Helmich\Schema2Class\Codegen\EnumGenerator; use Laminas\Code\Generator\ClassGenerator; -use Laminas\Code\Generator\EnumGenerator\EnumGenerator; use Laminas\Code\Generator\FileGenerator; trait GeneratorHookRunner diff --git a/src/Generator/Hook/EnumCreatedHook.php b/src/Generator/Hook/EnumCreatedHook.php index 684cf7f8..4ad17d59 100644 --- a/src/Generator/Hook/EnumCreatedHook.php +++ b/src/Generator/Hook/EnumCreatedHook.php @@ -2,7 +2,7 @@ namespace Helmich\Schema2Class\Generator\Hook; -use Laminas\Code\Generator\EnumGenerator\EnumGenerator; +use Helmich\Schema2Class\Codegen\EnumGenerator; /** * Interface definition for hooks that are called when an enumeration is created. diff --git a/src/Generator/SchemaToClass.php b/src/Generator/SchemaToClass.php index 4e8eed51..feb65a92 100644 --- a/src/Generator/SchemaToClass.php +++ b/src/Generator/SchemaToClass.php @@ -12,7 +12,6 @@ use Laminas\Code\Generator\ClassGenerator; use Laminas\Code\Generator\DocBlock\Tag\GenericTag; use Laminas\Code\Generator\DocBlockGenerator; -use Laminas\Code\Generator\EnumGenerator\EnumGenerator; use Laminas\Code\Generator\FileGenerator; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Generator/SchemaToEnum.php b/src/Generator/SchemaToEnum.php index 12c06c02..471ee28d 100644 --- a/src/Generator/SchemaToEnum.php +++ b/src/Generator/SchemaToEnum.php @@ -2,10 +2,12 @@ namespace Helmich\Schema2Class\Generator; +use Helmich\Schema2Class\Codegen\EnumGenerator; use Helmich\Schema2Class\Writer\WriterInterface; -use Laminas\Code\DeclareStatement; -use Laminas\Code\Generator\EnumGenerator\EnumGenerator; use Laminas\Code\Generator\FileGenerator; +use PhpParser\Builder\Enum_; +use PhpParser\Builder\EnumCase; +use PhpParser\Builder\Namespace_; class SchemaToEnum { @@ -39,24 +41,22 @@ public function schemaToEnum(GeneratorRequest $req): void $type = $req->getSchema()["type"] === "string" ? "string" : "int"; $enumName = $req->getTargetNamespace() . "\\" . $req->getTargetClass(); - $enum = EnumGenerator::withConfig([ - "name" => $enumName, - "backedCases" => [ - "type" => $type, - "cases" => $cases, - ], - ]); + $enumGenerator = new EnumGenerator( + enum_: (new Enum_($req->getTargetClass()))->setScalarType($type)->getNode(), + namespace_: (new Namespace_($req->getTargetNamespace()))->getNode(), + ); + foreach ($cases as $name => $value) { + $enumGenerator->withAdditionalEnumCase((new EnumCase($name))->setValue($value)->getNode()); + } - $req->onEnumCreated($enumName, $enum); + $req->onEnumCreated($enumName, $enumGenerator); $filename = $req->getTargetDirectory() . '/' . $req->getTargetClass() . '.php'; $file = new FileGenerator(); - $file->setBody($enum->generate()); + $file->setBody($enumGenerator->generate()); $req->onFileCreated($filename, $file); - $file->setDeclares([DeclareStatement::strictTypes(1)]); - $content = $file->generate(); // Do some corrections because the Zend code generation library is stupid. diff --git a/tests/Codegen/AbstractGeneratorTest.php b/tests/Codegen/AbstractGeneratorTest.php new file mode 100644 index 00000000..f550a655 --- /dev/null +++ b/tests/Codegen/AbstractGeneratorTest.php @@ -0,0 +1,96 @@ +expectException(\UnderflowException::class); + + $generator->last(); + } + + public function test_first_throws_exception_on_empty_nodes(): void + { + $generator = new class([]) extends AbstractGenerator {}; + + $this->expectException(\UnderflowException::class); + + $generator->last(); + } + + public function test_remove_and_set(): void + { + $firstNode = (new Namespace_('TestNamespace'))->getNode(); + $secondNode = (new Use_('TestImport', Node\Stmt\Use_::TYPE_NORMAL))->getNode(); + $thirdNode = (new Enum_('TestEnum'))->getNode(); + $setNode = (new Enum_('SetEnum'))->getNode(); + + $generator = new class([$firstNode, $secondNode, $thirdNode]) extends AbstractGenerator {}; + $removed = $generator->remove(1); + $generator->set(1, $setNode); + + $this->assertSame($secondNode, $removed); + $this->assertSame(2, $generator->count()); + $this->assertSame($firstNode, $generator->first()); + $this->assertSame($firstNode, $generator->get(0)); + $this->assertSame($setNode, $generator->last()); + } + + public function test_insert(): void + { + $firstNode = (new Namespace_('TestNamespaceInsert'))->getNode(); + $secondNode = (new Use_('TestImportInsert', Node\Stmt\Use_::TYPE_NORMAL))->getNode(); + $insertNode = (new Enum_('InsertedEnum'))->getNode(); + + $generator = new class([$firstNode, $secondNode]) extends AbstractGenerator {}; + $generator->insert(1, $insertNode); + + $this->assertSame(3, $generator->count()); + $this->assertSame($firstNode, $generator->first()); + $this->assertSame($insertNode, $generator->get(1)); + $this->assertSame($secondNode, $generator->last()); + } + + public function test_filter(): void + { + $firstNode = (new Namespace_('TestNamespaceInsert'))->getNode(); + $secondNode = (new Use_('TestImportInsert', Node\Stmt\Use_::TYPE_NORMAL))->getNode(); + $thirdNode = (new Enum_('InsertedEnum'))->getNode(); + + $generator = new class([$firstNode, $secondNode, $thirdNode]) extends AbstractGenerator {}; + $generator->filter(function (Node $value) { + return $value instanceof Node\Stmt\Enum_; + }); + + $this->assertSame(1, $generator->count()); + $this->assertSame($thirdNode, $generator->first()); + } + + public function test_walk(): void + { + $firstNode = (new Namespace_('TestNamespaceInsert'))->getNode(); + $secondNode = (new Use_('TestImportInsert', Node\Stmt\Use_::TYPE_NORMAL))->getNode(); + $replaceNode = (new Enum_('InsertedEnum'))->getNode(); + + $generator = new class([$firstNode, $secondNode]) extends AbstractGenerator {}; + $generator->walk(function (Node &$value) use ($replaceNode) { + $value = $replaceNode; + }); + + $this->assertSame(2, $generator->count()); + $this->assertSame($replaceNode, $generator->first()); + $this->assertSame($replaceNode, $generator->last()); + } +} \ No newline at end of file diff --git a/tests/Generator/Fixtures/Enum/Output/Foo.php b/tests/Generator/Fixtures/Enum/Output/Foo.php index c4376336..cd9df2bf 100644 --- a/tests/Generator/Fixtures/Enum/Output/Foo.php +++ b/tests/Generator/Fixtures/Enum/Output/Foo.php @@ -1,10 +1,11 @@ withHook(new class () implements EnumCreatedHook { + function onEnumCreated(string $enumName, EnumGenerator $enum): void + { + if ($enumName !== 'Ns\EnumHook\Foo') { + return; + } + + $enum->withAdditionalEnumCase((new EnumCase('blue'))->setValue('blue')->getNode()); + $enum->withEnum_((new Enum_('Foo')) + ->setScalarType('string') + ->setDocComment('/** Doc comment */') + ->getNode() + ); + } + }); $req = $req->withReferenceLookup(new class ($schema) implements ReferenceLookup { public function __construct(private readonly array $schema) {