From e467f1549a510dd6c0bddf3dea68b8b7642029c6 Mon Sep 17 00:00:00 2001 From: Constantine Karnaukhov Date: Mon, 1 Jun 2020 19:14:20 +0400 Subject: [PATCH] feat(type): add types --- CHANGELOG.md | 3 + src/BuiltinType.php | 208 ++++++++++++++++++++++++++++++++++ src/CollectionType.php | 157 +++++++++++++++++++++++++ src/ConjunctionType.php | 74 ++++++++++++ src/DisjunctionType.php | 74 ++++++++++++ src/InstanceOfType.php | 60 ++++++++++ src/Type.php | 35 ++++++ src/TypeFactory.php | 39 +++++++ tests/BuiltinTypeTest.php | 149 ++++++++++++++++++++++++ tests/CollectionTypeTest.php | 64 +++++++++++ tests/ConjunctionTypeTest.php | 49 ++++++++ tests/DisjunctionTypeTest.php | 43 +++++++ tests/InstanceOfTypeTest.php | 45 ++++++++ tests/TypeFactoryTest.php | 26 +++++ 14 files changed, 1026 insertions(+) create mode 100755 src/BuiltinType.php create mode 100755 src/CollectionType.php create mode 100755 src/ConjunctionType.php create mode 100755 src/DisjunctionType.php create mode 100755 src/InstanceOfType.php create mode 100755 src/Type.php create mode 100755 src/TypeFactory.php create mode 100755 tests/BuiltinTypeTest.php create mode 100755 tests/CollectionTypeTest.php create mode 100755 tests/ConjunctionTypeTest.php create mode 100755 tests/DisjunctionTypeTest.php create mode 100755 tests/InstanceOfTypeTest.php create mode 100755 tests/TypeFactoryTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff2033..ac989ad 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,3 +21,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Security - Nothing --> + +## [1.0.0] - 2020-06-01 +- First release diff --git a/src/BuiltinType.php b/src/BuiltinType.php new file mode 100755 index 0000000..0c07965 --- /dev/null +++ b/src/BuiltinType.php @@ -0,0 +1,208 @@ + true, + self::FLOAT => true, + self::STRING => true, + ]; + + /** + * @var string + */ + private $type; + /** + * @var bool + */ + private $strict; + + /** + * BuiltinType constructor. + * @param string $type + * @param bool $strict + */ + public function __construct(string $type, bool $strict = true) + { + if (!self::supports($type)) { + throw new InvalidArgumentException(sprintf('Type "%s" is not supported by %s', $type, __CLASS__)); + } + + $this->type = self::prepareType($type); + + if ($strict === false && !isset(self::SUPPORT_NO_STRICT[$this->type])) { + $strict = true; + trigger_error(sprintf('Type "%s" cannot be non-strict. $strict argument overridden.', $this->type)); + } + + $this->strict = $strict; + } + + /** + * @inheritDoc + */ + public function check($value): bool + { + try { + switch ($this->type) { + case self::INT: + if ($this->strict) { + Assert::integer($value); + } else { + Assert::integerish($value); + } + break; + + case self::FLOAT: + if ($this->strict) { + Assert::float($value); + } else { + Assert::numeric($value); + } + break; + + case self::STRING: + if ($this->strict) { + Assert::string($value); + } elseif (is_object($value)) { + Assert::methodExists($value, '__toString'); + } else { + Assert::scalar($value); + } + break; + + case self::BOOL: + Assert::boolean($value); + break; + + case self::RESOURCE: + Assert::resource($value); + break; + + case self::OBJECT: + Assert::object($value); + break; + + case self::ARRAY: + Assert::isArray($value); + break; + + case self::NULL: + Assert::null($value); + break; + + case self::CALLABLE: + Assert::isCallable($value); + break; + + case self::ITERABLE: + Assert::isIterable($value); + break; + } + + return true; + } catch (InvalidArgumentException $exception) { + return false; + } + } + + /** + * Cast value to current type + * @param mixed $value + * @return mixed + */ + public function cast($value) + { + switch ($this->type) { + case self::INT: + return (int)$value; + break; + + case self::FLOAT: + return (float)$value; + break; + + case self::STRING: + return (string)$value; + break; + + default: + return $value; + break; + } + } + + /** + * @inheritDoc + */ + public function __toString(): string + { + return $this->type; + } + + private static function prepareType(string $type): string + { + $type = strtolower($type); + + if (strpos($type, 'resource') === 0) { + $type = self::RESOURCE; + } + + $map = [ + 'boolean' => self::BOOL, + 'integer' => self::INT, + 'double' => self::FLOAT, + ]; + + return $map[$type] ?? $type; + } + + /** + * @inheritDoc + */ + public static function supports(string $type): bool + { + $type = self::prepareType($type); + + $supported = [ + self::INT => true, + self::FLOAT => true, + self::STRING => true, + self::BOOL => true, + self::RESOURCE => true, + self::OBJECT => true, + self::ARRAY => true, + self::NULL => true, + self::CALLABLE => true, + self::ITERABLE => true, + ]; + + return array_key_exists($type, $supported); + } + + /** + * @inheritDoc + */ + public static function create(string $type, bool $strict = true): Type + { + return new self($type, $strict); + } +} diff --git a/src/CollectionType.php b/src/CollectionType.php new file mode 100755 index 0000000..69e333f --- /dev/null +++ b/src/CollectionType.php @@ -0,0 +1,157 @@ +valueType = $valueType; + $this->keyType = $keyType; + $this->iterableType = $iterableType ?? new BuiltinType(BuiltinType::ITERABLE); + } + + /** + * @inheritDoc + */ + public function check($value): bool + { + if (!$this->iterableType->check($value)) { + return false; + } + + foreach ($value as $k => $v) { + if (!$this->valueType->check($v)) { + return false; + } + + if ($this->keyType !== null && !$this->keyType->check($k)) { + return false; + } + } + + return true; + } + + /** + * @inheritDoc + */ + public function __toString(): string + { + if ($this->iterableType instanceof InstanceOfType || $this->keyType !== null) { + return $this->iterableType . '<' . implode(',', array_filter([$this->keyType, $this->valueType])) . '>'; + } + + return $this->valueType . '[]'; + } + + /** + * @inheritDoc + */ + public static function supports(string $type): bool + { + $typeParts = self::parseType($type); + + if ($typeParts === null) { + return false; + } + + if (isset($typeParts['iterable'])) { + if (InstanceOfType::supports($typeParts['iterable'])) { + if ( + $typeParts['iterable'] !== Traversable::class && + !is_subclass_of($typeParts['iterable'], Traversable::class) + ) { + return false; + } + } else { + if (!in_array($typeParts['iterable'], ['array', 'iterable'], true)) { + return false; + } + } + } + + if (!isset($typeParts['value'])) { + return false; + } + + return true; + } + + /** + * @inheritDoc + */ + public static function create(string $type): Type + { + if (!self::supports($type)) { + throw new InvalidArgumentException(sprintf('Type "%s" is not supported by %s', $type, __CLASS__)); + } + + $parsed = self::parseType($type); + + $parsed['value'] = TypeFactory::create($parsed['value']); + $parsed['key'] = $parsed['key'] ? TypeFactory::create($parsed['key']) : null; + $parsed['iterable'] = $parsed['iterable'] ? TypeFactory::create($parsed['iterable']) : null; + + return new self($parsed['value'], $parsed['key'], $parsed['iterable']); + } + + private static function parseType(string $type): ?array + { + $result = [ + 'iterable' => null, + 'key' => null, + 'value' => null, + ]; + + if (strpos($type, '[]') === strlen($type) - 2) { + $result['value'] = substr($type, 0, -2) ?: null; + return $result; + } + + if (0 < ($openPos = strpos($type, '<')) && strpos($type, '>') === strlen($type) - 1) { + $result['iterable'] = substr($type, 0, $openPos); + [$key, $value] = array_map('trim', explode(',', substr($type, $openPos + 1, -1))) + [null, null]; + + if (!$value && !$key) { + return null; + } + + if ($value === null) { + $value = $key; + $key = null; + } + + $result['key'] = $key ?: null; + $result['value'] = $value ?: null; + + return $result; + } + + return null; + } +} diff --git a/src/ConjunctionType.php b/src/ConjunctionType.php new file mode 100755 index 0000000..3344884 --- /dev/null +++ b/src/ConjunctionType.php @@ -0,0 +1,74 @@ +conjuncts = $conjuncts; + } + + /** + * @inheritDoc + */ + public function check($value): bool + { + foreach ($this->conjuncts as $type) { + if (!$type->check($value)) { + return false; + } + } + + return true; + } + + /** + * @inheritDoc + */ + public function __toString(): string + { + return implode('&', array_map(static function (Type $type): string { + return (string)$type; + }, $this->conjuncts)); + } + + /** + * @inheritDoc + */ + public static function supports(string $type): bool + { + return count(explode('&', $type)) > 1; + } + + /** + * @inheritDoc + */ + public static function create(string $type): Type + { + if (!self::supports($type)) { + throw new InvalidArgumentException(sprintf('Type "%s" is not supported by %s', $type, __CLASS__)); + } + + $conjuncts = array_map(static function (string $subType): Type { + return TypeFactory::create(trim($subType)); + }, explode('&', $type)); + + return new self($conjuncts); + } +} diff --git a/src/DisjunctionType.php b/src/DisjunctionType.php new file mode 100755 index 0000000..16421d4 --- /dev/null +++ b/src/DisjunctionType.php @@ -0,0 +1,74 @@ +disjuncts = $disjuncts; + } + + /** + * @inheritDoc + */ + public function check($value): bool + { + foreach ($this->disjuncts as $type) { + if ($type->check($value)) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + public function __toString(): string + { + return implode('|', array_map(static function (Type $type): string { + return (string)$type; + }, $this->disjuncts)); + } + + /** + * @inheritDoc + */ + public static function supports(string $type): bool + { + return count(explode('|', $type)) > 1; + } + + /** + * @inheritDoc + */ + public static function create(string $type): Type + { + if (!self::supports($type)) { + throw new InvalidArgumentException(sprintf('Type "%s" is not supported by %s', $type, __CLASS__)); + } + + $disjuncts = array_map(static function (string $subType): Type { + return TypeFactory::create(trim($subType)); + }, explode('|', $type)); + + return new self($disjuncts); + } +} diff --git a/src/InstanceOfType.php b/src/InstanceOfType.php new file mode 100755 index 0000000..63a5873 --- /dev/null +++ b/src/InstanceOfType.php @@ -0,0 +1,60 @@ +className = $className; + } + + /** + * @inheritDoc + */ + public function check($value): bool + { + return $value instanceof $this->className; + } + + /** + * @inheritDoc + */ + public function __toString(): string + { + return $this->className; + } + + /** + * @inheritDoc + */ + public static function supports(string $type): bool + { + return class_exists($type) || interface_exists($type); + } + + /** + * @inheritDoc + */ + public static function create(string $type): Type + { + return new self($type); + } +} diff --git a/src/Type.php b/src/Type.php new file mode 100755 index 0000000..c79b87d --- /dev/null +++ b/src/Type.php @@ -0,0 +1,35 @@ +expectException(InvalidArgumentException::class); + BuiltinType::create('unknown type'); + } + + public function testCheck(): void + { + $integer = BuiltinType::create('integer'); + self::assertTrue($integer->check(1)); + self::assertFalse($integer->check('1')); + + $float = BuiltinType::create('float'); + self::assertTrue($float->check(1.0)); + self::assertFalse($float->check('1')); + + $string = BuiltinType::create('string'); + self::assertTrue($string->check('lorem ipsum')); + self::assertFalse($string->check(1)); + + $bool = BuiltinType::create('bool'); + self::assertTrue($bool->check(true)); + self::assertFalse($bool->check(1)); + + $object = BuiltinType::create('object'); + self::assertTrue($object->check((object)[])); + self::assertFalse($object->check(1)); + + $array = BuiltinType::create('array'); + self::assertTrue($array->check([])); + self::assertFalse($array->check(1)); + + $null = BuiltinType::create('null'); + self::assertTrue($null->check(null)); + self::assertFalse($null->check(1)); + + $callable = BuiltinType::create('callable'); + self::assertTrue($callable->check(static function () { + })); + self::assertFalse($callable->check(1)); + + $iterable = BuiltinType::create('iterable'); + self::assertTrue($iterable->check(new ArrayIterator())); + self::assertFalse($iterable->check(1)); + + $f = fopen(__FILE__, 'rb'); + $resource = BuiltinType::create('resource'); + self::assertTrue($resource->check($f)); + self::assertFalse($resource->check(1)); + fclose($f); + } + + public function testNonStrictCheck(): void + { + $integer = BuiltinType::create('integer', false); + self::assertTrue($integer->check(1)); + self::assertTrue($integer->check('1')); + self::assertFalse($integer->check('lorem ipsum')); + + $float = BuiltinType::create('float', false); + self::assertTrue($float->check(1.0)); + self::assertTrue($float->check('1.1')); + self::assertFalse($float->check('lorem ipsum')); + + $string = BuiltinType::create('string', false); + self::assertTrue($string->check('lorem ipsum')); + self::assertTrue($string->check(1)); + self::assertTrue($string->check(true)); + self::assertTrue($string->check(new class { + public function __toString(): string + { + return 'lorem ipsum'; + } + })); + self::assertFalse($string->check(null)); + self::assertFalse($string->check([])); + } + + public function testNonStrictNotice(): void + { + $this->expectNotice(); + BuiltinType::create('object', false); + } + + public function testCast(): void + { + $integer = BuiltinType::create('integer'); + self::assertEquals(1, $integer->cast('1')); + + $float = BuiltinType::create('float'); + self::assertEquals(1.0, $float->cast('1.0')); + + $string = BuiltinType::create('string'); + self::assertEquals('lorem ipsum', $string->cast(new class { + public function __toString(): string + { + return 'lorem ipsum'; + } + })); + + $bool = BuiltinType::create('bool'); + self::assertEquals('1', $bool->cast('1')); + } + + public function testStringify(): void + { + $type = BuiltinType::create('integer'); + self::assertEquals('int', (string)$type); + } +} diff --git a/tests/CollectionTypeTest.php b/tests/CollectionTypeTest.php new file mode 100755 index 0000000..feeaadd --- /dev/null +++ b/tests/CollectionTypeTest.php @@ -0,0 +1,64 @@ +')); + self::assertTrue(CollectionType::supports('iterable')); + self::assertTrue(CollectionType::supports('ArrayIterator')); + self::assertTrue(CollectionType::supports('Traversable')); + self::assertFalse(CollectionType::supports('[]')); + self::assertFalse(CollectionType::supports('<>')); + self::assertFalse(CollectionType::supports('ArrayIterator<>')); + self::assertFalse(CollectionType::supports('stdClass<>')); + self::assertFalse(CollectionType::supports('string')); + } + + public function testCreate(): void + { + CollectionType::create('int[]'); + CollectionType::create('array'); + CollectionType::create('iterable'); + CollectionType::create('ArrayIterator'); + CollectionType::create('Traversable'); + self::assertTrue(true); + } + + public function testCreateNotSupported(): void + { + $this->expectException(InvalidArgumentException::class); + CollectionType::create('<>'); + } + + public function testCheck(): void + { + $integerCollection = CollectionType::create('int[]'); + self::assertTrue($integerCollection->check([1, 2, 'c' => 3])); + self::assertFalse($integerCollection->check(['a', 'b', 'c'])); + + $arrayStringInt = CollectionType::create('array'); + self::assertTrue($arrayStringInt->check(['a' => 1, 'b' => 2, 'c' => 3])); + self::assertFalse($arrayStringInt->check([1, 2, 'c' => 3])); + self::assertFalse($arrayStringInt->check(new ArrayIterator())); + + $arrayIteratorInteger = CollectionType::create('ArrayIterator'); + self::assertTrue($arrayIteratorInteger->check(new ArrayIterator([1, 2, 'c' => 3]))); + } + + public function testStringify(): void + { + self::assertEquals('int[]', (string)CollectionType::create('int[]')); + self::assertEquals('array', (string)CollectionType::create('array')); + self::assertEquals('ArrayIterator', (string)CollectionType::create('ArrayIterator')); + } +} diff --git a/tests/ConjunctionTypeTest.php b/tests/ConjunctionTypeTest.php new file mode 100755 index 0000000..64b4a64 --- /dev/null +++ b/tests/ConjunctionTypeTest.php @@ -0,0 +1,49 @@ +expectException(InvalidArgumentException::class); + ConjunctionType::create(JsonSerializable::class); + } + + public function testCheck(): void + { + $type = ConjunctionType::create(JsonSerializable::class . '&' . Traversable::class); + + $jsonSerializable = $this->prophesize(JsonSerializable::class)->reveal(); + $jsonSerializableAndTraversable = $this->prophesize(JsonSerializable::class)->willImplement(Iterator::class)->reveal(); + + self::assertTrue($type->check($jsonSerializableAndTraversable)); + self::assertFalse($type->check($jsonSerializable)); + } + + public function testStringify(): void + { + $type = ConjunctionType::create(JsonSerializable::class . '&' . Traversable::class); + self::assertEquals(JsonSerializable::class . '&' . Traversable::class, (string)$type); + } +} diff --git a/tests/DisjunctionTypeTest.php b/tests/DisjunctionTypeTest.php new file mode 100755 index 0000000..1e1e44d --- /dev/null +++ b/tests/DisjunctionTypeTest.php @@ -0,0 +1,43 @@ +expectException(InvalidArgumentException::class); + DisjunctionType::create('int'); + } + + public function testCheck(): void + { + $type = DisjunctionType::create('integer|null'); + self::assertTrue($type->check(1)); + self::assertTrue($type->check(null)); + self::assertFalse($type->check('1')); + } + + public function testStringify(): void + { + $type = DisjunctionType::create('integer|null'); + self::assertEquals('int|null', (string)$type); + } +} diff --git a/tests/InstanceOfTypeTest.php b/tests/InstanceOfTypeTest.php new file mode 100755 index 0000000..6f1513a --- /dev/null +++ b/tests/InstanceOfTypeTest.php @@ -0,0 +1,45 @@ +expectException(InvalidArgumentException::class); + InstanceOfType::create('NonExistingClass'); + } + + public function testCheck(): void + { + $type = InstanceOfType::create(stdClass::class); + self::assertTrue($type->check((object)[])); + self::assertFalse($type->check([])); + } + + public function testStringify(): void + { + $type = InstanceOfType::create(stdClass::class); + self::assertEquals(stdClass::class, (string)$type); + } +} diff --git a/tests/TypeFactoryTest.php b/tests/TypeFactoryTest.php new file mode 100755 index 0000000..8046fef --- /dev/null +++ b/tests/TypeFactoryTest.php @@ -0,0 +1,26 @@ +expectException(InvalidArgumentException::class); + self::assertInstanceOf(BuiltinType::class, TypeFactory::create('unknown type')); + } +}