diff --git a/src/Common/IteratorFactory.php b/src/Common/IteratorFactory.php new file mode 100644 index 0000000..1eaebb1 --- /dev/null +++ b/src/Common/IteratorFactory.php @@ -0,0 +1,177 @@ +parser = $parser; + $this->typed = $typed; + $this->propertyFactory = new PropertyFactory($typed); + } + + /** + * @param string $name + * @param string $itemType + * @param string $positionPropertyName + * @return array + */ + public function nodeVisitorsFromNative( + string $name, + string $itemType, + string $positionPropertyName = 'position' + ): array { + $nodeVisitors = []; + + $nodeVisitors[] = new Property( + $this->propertyFactory->propertyGenerator($positionPropertyName, 'int')->setDefaultValue(0) + ); + $nodeVisitors[] = new Property( + $this->propertyFactory->propertyGenerator($name, 'array')->setTypeDocBlockHint($itemType . '[]') + ); + + $nodeVisitors[] = new ClassMethod($this->methodRewind($positionPropertyName)); + $nodeVisitors[] = new ClassMethod($this->methodCurrent($name, $itemType, $positionPropertyName)); + $nodeVisitors[] = new ClassMethod($this->methodKey($positionPropertyName)); + $nodeVisitors[] = new ClassMethod($this->methodNext($positionPropertyName)); + $nodeVisitors[] = new ClassMethod($this->methodValid($name, $positionPropertyName)); + $nodeVisitors[] = new ClassMethod($this->methodCount($name)); + + $nodeVisitors[] = new ClassImplements('\\Iterator', '\\Countable'); + + return $nodeVisitors; + } + + public function classBuilderFromNative( + string $name, + string $itemType, + string $positionPropertyName = 'position' + ): ClassBuilder { + return ClassBuilder::fromNodes( + $this->propertyFactory->propertyGenerator($positionPropertyName, 'int')->setDefaultValue(0)->generate(), + $this->propertyFactory->propertyGenerator($name, 'array')->setTypeDocBlockHint($itemType . '[]')->generate(), + $this->methodRewind($positionPropertyName)->generate(), + $this->methodCurrent($name, $itemType, $positionPropertyName)->generate(), + $this->methodKey($positionPropertyName)->generate(), + $this->methodNext($positionPropertyName)->generate(), + $this->methodValid($name, $positionPropertyName)->generate(), + $this->methodCount($name)->generate(), + )->setImplements('\\Iterator', '\\Countable') + ->setTyped($this->typed); + } + + public function methodRewind(string $positionPropertyName): MethodGenerator + { + $method = new MethodGenerator( + 'rewind', + [], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, '$this->' . $positionPropertyName . ' = 0;') + ); + $method->setTyped($this->typed); + $method->setReturnType('void'); + + return $method; + } + + public function methodCurrent(string $name, string $itemType, string $positionPropertyName): MethodGenerator + { + $method = new MethodGenerator( + 'current', + [], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, \sprintf('return $this->%s[$this->%s];', $name, $positionPropertyName)) + ); + $method->setTyped($this->typed); + $method->setReturnType($itemType); + + return $method; + } + + public function methodKey(string $positionPropertyName): MethodGenerator + { + $method = new MethodGenerator( + 'key', + [], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, 'return $this->' . $positionPropertyName . ';') + ); + $method->setTyped($this->typed); + $method->setReturnType('int'); + + return $method; + } + + public function methodNext(string $positionPropertyName): MethodGenerator + { + $method = new MethodGenerator( + 'next', + [], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, '++$this->' . $positionPropertyName . ';') + ); + $method->setTyped($this->typed); + $method->setReturnType('void'); + + return $method; + } + + public function methodValid(string $name, string $positionPropertyName): MethodGenerator + { + $method = new MethodGenerator( + 'valid', + [], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator( + $this->parser, + \sprintf('return isset($this->%s[$this->%s]);', $name, $positionPropertyName) + ) + ); + $method->setTyped($this->typed); + $method->setReturnType('bool'); + + return $method; + } + + public function methodCount(string $name): MethodGenerator + { + $method = new MethodGenerator( + 'count', + [], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, 'return count($this->' . $name . ');') + ); + $method->setTyped($this->typed); + $method->setReturnType('int'); + + return $method; + } +} diff --git a/src/ValueObject/ArrayFactory.php b/src/ValueObject/ArrayFactory.php new file mode 100644 index 0000000..2a3f93a --- /dev/null +++ b/src/ValueObject/ArrayFactory.php @@ -0,0 +1,467 @@ +parser = $parser; + $this->typed = $typed; + $this->classNameFilter = $classNameFilter; + $this->propertyNameFilter = $propertyNameFilter; + $this->propertyFactory = new PropertyFactory($typed); + $this->iteratorFactory = new IteratorFactory($parser, $typed); + } + + /** + * @param ArrayType $typeDefinition + * @return array + */ + public function nodeVisitors(ArrayType $typeDefinition): array + { + $name = $typeDefinition->name() ?: 'items'; + + return $this->nodeVisitorsFromNative($name, ...$typeDefinition->items()); + } + + public function classBuilder(ArrayType $typeDefinition): ClassBuilder + { + $name = $typeDefinition->name() ?: 'items'; + + return $this->classBuilderFromNative($name, ...$typeDefinition->items()); + } + + private function determineType(string $name, TypeSet ...$typeSets): TypeDefinition + { + if (\count($typeSets) !== 1) { + throw new \RuntimeException('Can only handle one JSON type'); + } + $typeSet = \array_shift($typeSets); + + // @phpstan-ignore-next-line + if ($typeSet === null || $typeSet->count() !== 1) { + throw new \RuntimeException('Can only handle one JSON type'); + } + + $type = $typeSet->first(); + + switch (true) { + case $type instanceof ReferenceType: + $resolvedTypeSet = $type->resolvedType(); + + if ($resolvedTypeSet === null) { + throw new \RuntimeException(\sprintf('Reference has no resolved type for "%s".', $name)); + } + + if (\count($resolvedTypeSet) !== 1) { + throw new \RuntimeException('Can only handle one JSON type'); + } + $type = $typeSet->first(); + break; + case $type instanceof ScalarType: + break; + default: + throw new \RuntimeException( + \sprintf('Only scalar and reference types are supported. Got "%s" for "%s"', \get_class($type), $name) + ); + } + + return $type; + } + + /** + * @param string $name + * @param TypeSet ...$typeSets + * @return array + */ + public function nodeVisitorsFromNative(string $name, TypeSet ...$typeSets): array + { + $type = $this->determineType($name, ...$typeSets); + $typeName = $type->name(); + + $nodeVisitors = $this->iteratorFactory->nodeVisitorsFromNative( + ($this->propertyNameFilter)($name), + ($this->classNameFilter)($typeName) + ); + + $nodeVisitors[] = new ClassMethod($this->methodFromArray($name, $typeName)); + $nodeVisitors[] = new ClassMethod($this->methodFromItems($name, $typeName)); + $nodeVisitors[] = new ClassMethod($this->methodEmptyList()); + $nodeVisitors[] = new ClassMethod($this->methodMagicConstruct($name, $name, $typeName)); + $nodeVisitors[] = new ClassMethod($this->methodAdd($name, $typeName)); + $nodeVisitors[] = new ClassMethod($this->methodRemove($name, $typeName)); + $nodeVisitors[] = new ClassMethod($this->methodFirst($name, $typeName)); + $nodeVisitors[] = new ClassMethod($this->methodLast($name, $typeName)); + $nodeVisitors[] = new ClassMethod($this->methodContains($name, $typeName)); + $nodeVisitors[] = new ClassMethod($this->methodFilter($name)); + $nodeVisitors[] = new ClassMethod($this->methodItems($name, $typeName)); + $nodeVisitors[] = new ClassMethod($this->methodToArray($name, $typeName)); + $nodeVisitors[] = new ClassMethod($this->methodEquals()); + + return $nodeVisitors; + } + + public function classBuilderFromNative(string $name, TypeSet ...$typeSets): ClassBuilder + { + $type = $this->determineType($name, ...$typeSets); + $typeName = $type->name(); + + $classBuilder = $this->iteratorFactory->classBuilderFromNative( + ($this->propertyNameFilter)($name), + ($this->classNameFilter)($typeName) + ); + + return $classBuilder; + } + + public function methodEmptyList(): MethodGenerator + { + $method = new MethodGenerator( + 'emptyList', + [], + MethodGenerator::FLAG_STATIC | MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, 'return new self();') + ); + $method->setTyped($this->typed); + $method->setReturnType('self'); + + return $method; + } + + public function methodMagicConstruct( + string $propertyName, + string $argumentName, + string $argumentType + ): MethodGenerator { + $method = new MethodGenerator( + '__construct', + [ + (new ParameterGenerator($argumentName, ($this->classNameFilter)($argumentType)))->setVariadic(true), + ], + MethodGenerator::FLAG_PRIVATE, + new BodyGenerator($this->parser, \sprintf('$this->%s = $%s;', $propertyName, $argumentName)) + ); + $method->setTyped($this->typed); + + return $method; + } + + public function methodAdd( + string $propertyName, + string $argumentType + ): MethodGenerator { + $body = <<<'PHP' + $copy = clone $this; + $copy->%s[] = $%s; + + return $copy; +PHP; + + $method = new MethodGenerator( + 'add', + [ + (new ParameterGenerator(($this->propertyNameFilter)($argumentType), ($this->classNameFilter)($argumentType))), + ], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, \sprintf($body, $propertyName, ($this->propertyNameFilter)($argumentType))) + ); + $method->setTyped($this->typed); + $method->setReturnType('self'); + + return $method; + } + + public function methodRemove( + string $propertyName, + string $argumentType + ): MethodGenerator { + $body = <<<'PHP' + $copy = clone $this; + $copy->%s = array_values( + array_filter( + $copy->%s, + static function($v) { return !$v->equals($%s); } + ) + ); + return $copy; +PHP; + + $method = new MethodGenerator( + 'remove', + [ + (new ParameterGenerator(($this->propertyNameFilter)($argumentType), ($this->classNameFilter)($argumentType))), + ], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, \sprintf($body, $propertyName, $propertyName, ($this->propertyNameFilter)($argumentType))) + ); + $method->setTyped($this->typed); + $method->setReturnType('self'); + + return $method; + } + + public function methodFirst( + string $propertyName, + string $argumentType + ): MethodGenerator { + $method = new MethodGenerator( + 'first', + [], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, \sprintf('return $this->%s[0] ?? null;', ($this->propertyNameFilter)($propertyName))) + ); + $method->setTyped($this->typed); + $method->setReturnType('?' . ($this->classNameFilter)($argumentType)); + + return $method; + } + + public function methodLast( + string $propertyName, + string $argumentType + ): MethodGenerator { + $body = <<<'PHP' + if (count($this->%s) === 0) { + return null; + } + + return $this->%s[count($this->%s) - 1]; +PHP; + + $propertyName = ($this->propertyNameFilter)($propertyName); + $argumentType = ($this->classNameFilter)($argumentType); + + $method = new MethodGenerator( + 'last', + [], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, \sprintf($body, $propertyName, $propertyName, $propertyName)) + ); + $method->setTyped($this->typed); + $method->setReturnType('?' . $argumentType); + + return $method; + } + + public function methodContains( + string $propertyName, + string $argumentType + ): MethodGenerator { + $body = <<<'PHP' + foreach ($this->%s as $existingItem) { + if ($existingItem->equals($%s)) { + return true; + } + } + return false; +PHP; + + $propertyName = ($this->propertyNameFilter)($propertyName); + $argumentType = ($this->classNameFilter)($argumentType); + + $method = new MethodGenerator( + 'contains', + [ + (new ParameterGenerator(($this->propertyNameFilter)($argumentType), ($this->classNameFilter)($argumentType))), + ], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, \sprintf($body, $propertyName, ($this->propertyNameFilter)($argumentType))) + ); + $method->setTyped($this->typed); + $method->setReturnType('bool'); + + return $method; + } + + public function methodFilter( + string $propertyName + ): MethodGenerator { + $body = <<<'PHP' + return new self( + ...array_values( + array_filter( + $this->%s, + static function($%s) { return $filter($%s); } + ) + ) + ); +PHP; + + $method = new MethodGenerator( + 'filter', + [ + new ParameterGenerator('filter', 'callable'), + ], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, \sprintf($body, $propertyName, 'v', 'v')) + ); + $method->setTyped($this->typed); + $method->setReturnType('self'); + + return $method; + } + + public function methodItems( + string $propertyName, + string $argumentType + ): MethodGenerator { + $propertyName = ($this->propertyNameFilter)($propertyName); + $argumentType = ($this->classNameFilter)($argumentType); + + $method = new MethodGenerator( + 'items', + [], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, \sprintf('return $this->%s;', $propertyName)) + ); + $method->setTyped($this->typed); + $method->setReturnType('array'); + $method->setReturnTypeDocBlockHint($argumentType . '[]'); + + return $method; + } + + public function methodToArray( + string $propertyName, + string $argumentType + ): MethodGenerator { + $body = <<<'PHP' + return \array_map(static function (%s $%s) { + return $%s->toArray(); + }, $this->%s); +PHP; + + $propertyName = ($this->propertyNameFilter)($propertyName); + $argumentType = ($this->classNameFilter)($argumentType); + $argumentTypeVarName = ($this->propertyNameFilter)($argumentType); + + $method = new MethodGenerator( + 'toArray', + [], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, \sprintf($body, $argumentType, $argumentTypeVarName, $argumentTypeVarName, $propertyName)) + ); + $method->setTyped($this->typed); + $method->setReturnType('array'); + + return $method; + } + + public function methodFromArray( + string $argumentName, + string $typeName + ): MethodGenerator { + $body = <<<'PHP' + return new self(...array_map(static function (string $item) { + return %s::fromString($item); + }, $%s)); +PHP; + $argumentName = ($this->propertyNameFilter)($argumentName); + $typeName = ($this->classNameFilter)($typeName); + + $method = new MethodGenerator( + 'fromArray', + [ + new ParameterGenerator($argumentName, 'array'), + ], + MethodGenerator::FLAG_PUBLIC | MethodGenerator::FLAG_STATIC, + new BodyGenerator($this->parser, \sprintf($body, $typeName, $argumentName)) + ); + $method->setTyped($this->typed); + $method->setReturnType('self'); + + return $method; + } + + public function methodFromItems( + string $argumentName, + string $argumentType + ): MethodGenerator { + $method = new MethodGenerator( + 'fromItems', + [ + (new ParameterGenerator($argumentName, ($this->classNameFilter)($argumentType)))->setVariadic(true), + ], + MethodGenerator::FLAG_PUBLIC | MethodGenerator::FLAG_STATIC, + new BodyGenerator($this->parser, \sprintf('return new self(...$%s);', $argumentName)) + ); + $method->setTyped($this->typed); + $method->setReturnType('self'); + + return $method; + } + + public function methodEquals(string $argumentName = 'other'): MethodGenerator + { + $body = <<<'PHP' + if(!$%s instanceof self) { + return false; + } + + return $this->toArray() === $%s->toArray(); +PHP; + + $method = new MethodGenerator( + 'equals', + [ + (new ParameterGenerator($argumentName))->setTypeDocBlockHint('mixed'), + ], + MethodGenerator::FLAG_PUBLIC, + new BodyGenerator($this->parser, \sprintf($body, $argumentName, $argumentName)) + ); + $method->setDocBlockComment(''); + $method->setTyped($this->typed); + $method->setReturnType('bool'); + + return $method; + } +} diff --git a/tests/Common/IteratorFactoryTest.php b/tests/Common/IteratorFactoryTest.php new file mode 100644 index 0000000..650bb32 --- /dev/null +++ b/tests/Common/IteratorFactoryTest.php @@ -0,0 +1,97 @@ +iteratorFactory = new IteratorFactory($this->parser, true); + } + + /** + * @test + */ + public function it_generates_code_from_native() : void + { + $this->assertCode($this->iteratorFactory->nodeVisitorsFromNative('reasonTypes', 'ReasonType')); + } + + /** + * @test + */ + public function it_generates_code_from_native_with_class_builder() : void + { + $this->assertCode( + $this->iteratorFactory->classBuilderFromNative('reasonTypes', 'ReasonType') + ->setName('IteratorVO') + ->generate($this->parser) + ); + } + + /** + * @param array $nodeVisitors + */ + private function assertCode(array $nodeVisitors): void + { + $ast = $this->parser->parse('addVisitor($nodeVisitor); + } + + $expected = <<<'EOF' +position = 0; + } + public function current() : ReasonType + { + return $this->reasonTypes[$this->position]; + } + public function key() : int + { + return $this->position; + } + public function next() : void + { + ++$this->position; + } + public function valid() : bool + { + return isset($this->reasonTypes[$this->position]); + } + public function count() : int + { + return count($this->reasonTypes); + } +} +EOF; + + $this->assertSame($expected, $this->printer->prettyPrintFile($nodeTraverser->traverse($ast))); + } +} diff --git a/tests/ValueObject/ArrayFactoryTest.php b/tests/ValueObject/ArrayFactoryTest.php new file mode 100644 index 0000000..a4b2e09 --- /dev/null +++ b/tests/ValueObject/ArrayFactoryTest.php @@ -0,0 +1,217 @@ +arrayFactory = new ArrayFactory( + $this->parser, + true, + function(string $value) { return \ucfirst(($this->filterConstValue)($value)); }, + $this->filterConstValue + ); + } + + /** + * @test + */ + public function it_generates_code_from_native(): void + { + $this->assertCode($this->arrayFactory->nodeVisitorsFromNative('items')); + } + + /** + * @test + */ + public function it_generates_code_from_definition(): void + { + $definition = json_decode( + file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'schema_with_array_type_ref.json'), + true + ); + $this->assertCode($this->arrayFactory->nodeVisitors(Type::fromDefinition($definition)->first())); + } + + /** + * @test + */ + public function it_generates_code_via_value_object_factory(): void + { + $definition = json_decode( + file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'schema_with_array_type_ref.json'), + true + ); + + $this->assertCode( + $this->voFactory->nodeVisitors( + ArrayType::fromDefinition($definition) + ) + ); + } + + /** + * @test + */ + public function it_generates_code_via_value_object_factory_with_class_builder(): void + { + $definition = json_decode( + file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'schema_with_array_type_ref.json'), + true + ); + + $classBuilder = $this->voFactory->classBuilder(ArrayType::fromDefinition($definition)); + $classBuilder->setName('ReasonTypeListVO'); + + $this->assertCode( + $classBuilder->generate($this->parser) + ); + } + + /** + * @param array $nodeVisitors + */ + private function assertCode(array $nodeVisitors): void + { + $ast = $this->parser->parse('addVisitor($nodeVisitor); + } + + $expected = <<<'EOF' +position = 0; + } + public function current() : ReasonType + { + return $this->items[$this->position]; + } + public function key() : int + { + return $this->position; + } + public function next() : void + { + ++$this->position; + } + public function valid() : bool + { + return isset($this->items[$this->position]); + } + public function count() : int + { + return count($this->items); + } + public static function fromArray(array $items) : self + { + return new self(...array_map(static function (string $item) { + return ReasonType::fromString($item); + }, $items)); + } + public static function fromItems(ReasonType ...$items) : self + { + return new self(...$items); + } + public static function emptyList() : self + { + return new self(); + } + private function __construct(ReasonType ...$items) + { + $this->items = $items; + } + public function add(ReasonType $reasonType) : self + { + $copy = clone $this; + $copy->items[] = $reasonType; + return $copy; + } + public function remove(ReasonType $reasonType) : self + { + $copy = clone $this; + $copy->items = array_values(array_filter($copy->items, static function ($v) { + return !$v->equals($reasonType); + })); + return $copy; + } + public function first() : ?ReasonType + { + return $this->items[0] ?? null; + } + public function last() : ?ReasonType + { + if (count($this->items) === 0) { + return null; + } + return $this->items[count($this->items) - 1]; + } + public function contains(ReasonType $reasonType) : bool + { + foreach ($this->items as $existingItem) { + if ($existingItem->equals($reasonType)) { + return true; + } + } + return false; + } + public function filter(callable $filter) : self + { + return new self(...array_values(array_filter($this->items, static function ($v) { + return $filter($v); + }))); + } + /** + * @return ReasonType[] + */ + public function items() : array + { + return $this->items; + } + public function toArray() : array + { + return \array_map(static function (ReasonType $reasonType) { + return $reasonType->toArray(); + }, $this->items); + } + /** + * @param mixed $other + * @return bool + */ + public function equals($other) : bool + { + if (!$other instanceof self) { + return false; + } + return $this->toArray() === $other->toArray(); + } +} +EOF; + + $this->assertSame($expected, $this->printer->prettyPrintFile($nodeTraverser->traverse($ast))); + } +} diff --git a/tests/ValueObject/_files/schema_with_array_type_ref.json b/tests/ValueObject/_files/schema_with_array_type_ref.json new file mode 100644 index 0000000..e2e4223 --- /dev/null +++ b/tests/ValueObject/_files/schema_with_array_type_ref.json @@ -0,0 +1,12 @@ +{ + "type": "array", + "items": { + "$ref": "#/definitions/reasonType" + }, + "definitions": { + "reasonType": { + "type": "string", + "enum": ["not_interested", "invalid"] + } + } +}