Skip to content

Commit

Permalink
add 'literal_scalar' type
Browse files Browse the repository at this point in the history
  • Loading branch information
azjezz committed Mar 14, 2021
1 parent ec7398e commit c5ede33
Show file tree
Hide file tree
Showing 40 changed files with 483 additions and 46 deletions.
2 changes: 2 additions & 0 deletions src/Psl/Internal/Loader.php
Expand Up @@ -410,6 +410,7 @@ final class Loader
'Psl\Type\is_resource',
'Psl\Type\is_scalar',
'Psl\Type\is_string',
'Psl\Type\literal_scalar',
'Psl\Json\encode',
'Psl\Json\decode',
'Psl\Json\typed',
Expand Down Expand Up @@ -576,6 +577,7 @@ final class Loader
'Psl\Type\Internal\VecType',
'Psl\Type\Internal\DictType',
'Psl\Type\Internal\ScalarType',
'Psl\Type\Internal\LiteralScalarType',
'Psl\Type\Exception\TypeTrace',
'Psl\Type\Exception\AssertException',
'Psl\Type\Exception\CoercionException',
Expand Down
2 changes: 1 addition & 1 deletion src/Psl/Type/Exception/Exception.php
Expand Up @@ -7,7 +7,7 @@
use Psl\Exception\RuntimeException;
use Psl\Str;

abstract class Exception extends RuntimeException
abstract class Exception extends RuntimeException implements ExceptionInterface
{
private TypeTrace $typeTrace;
private string $actual;
Expand Down
1 change: 1 addition & 0 deletions src/Psl/Type/Internal/ArrayKeyType.php
Expand Up @@ -13,6 +13,7 @@ final class ArrayKeyType extends UnionType
{
public function __construct()
{
/** @psalm-suppress MissingThrowsDocblock */
parent::__construct(new StringType(), new IntType());
}

Expand Down
4 changes: 2 additions & 2 deletions src/Psl/Type/Internal/BoolType.php
Expand Up @@ -36,11 +36,11 @@ public function coerce($value): bool
return $value;
}

if (0 === $value) {
if (0 === $value || '0' === $value) {
return false;
}

if (1 === $value) {
if (1 === $value || '1' === $value) {
return true;
}

Expand Down
35 changes: 15 additions & 20 deletions src/Psl/Type/Internal/DictType.php
Expand Up @@ -4,7 +4,7 @@

namespace Psl\Type\Internal;

use Psl\Dict;
use Psl;
use Psl\Str;
use Psl\Type;
use Psl\Type\Exception\AssertException;
Expand Down Expand Up @@ -36,11 +36,18 @@ final class DictType extends Type\Type
/**
* @param Type\TypeInterface<Tk> $key_type
* @param Type\TypeInterface<Tv> $value_type
*
* @throws Psl\Exception\InvariantViolationException If $key_value, or $value_type is optional.
*/
public function __construct(
Type\TypeInterface $key_type,
Type\TypeInterface $value_type
) {
Psl\invariant(
!$key_type->isOptional() && !$value_type->isOptional(),
'Optional type must be the outermost.'
);

$this->key_type = $key_type;
$this->value_type = $value_type;
}
Expand All @@ -63,23 +70,17 @@ public function coerce($value): array
$key_type = $this->key_type->withTrace($key_trace);
$value_type = $this->value_type->withTrace($value_trace);

/**
* @var list<array{0: Tk, 1: Tv}> $entries
*/
$entries = [];
$result = [];

/**
* @var Tk $k
* @var Tv $v
*/
foreach ($value as $k => $v) {
$entries[] = [
$key_type->coerce($k),
$value_type->coerce($v),
];
$result[$key_type->coerce($k)] = $value_type->coerce($v);
}

return Dict\from_entries($entries);
return $result;
}

throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
Expand All @@ -105,23 +106,17 @@ public function assert($value): array
$key_type = $this->key_type->withTrace($key_trace);
$value_type = $this->value_type->withTrace($value_trace);

/**
* @var list<array{0: Tk, 1: Tv}> $entries
*/
$entries = [];
$result = [];

/**
* @var Tk $k
* @var Tv $v
*/
foreach ($value as $k => $v) {
$entries[] = [
$key_type->assert($k),
$value_type->assert($v),
];
$result[$key_type->assert($k)] = $value_type->assert($v);
}

return Dict\from_entries($entries);
return $result;
}

throw AssertException::withValue($value, $this->toString(), $this->getTrace());
Expand Down
9 changes: 9 additions & 0 deletions src/Psl/Type/Internal/IntType.php
Expand Up @@ -9,6 +9,7 @@
use Psl\Type\Exception\AssertException;
use Psl\Type\Exception\CoercionException;

use function is_float;
use function is_int;
use function is_object;
use function is_string;
Expand Down Expand Up @@ -59,6 +60,14 @@ public function coerce($value): int
return 0;
}
}

if (is_float($value)) {
$integer_value = (int) $value;
$reconstructed = (float) $integer_value;
if ($reconstructed === $value) {
return $integer_value;
}
}

throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
}
Expand Down
8 changes: 8 additions & 0 deletions src/Psl/Type/Internal/IntersectionType.php
Expand Up @@ -4,6 +4,7 @@

namespace Psl\Type\Internal;

use Psl;
use Psl\Str;
use Psl\Type\Exception\AssertException;
use Psl\Type\Exception\CoercionException;
Expand Down Expand Up @@ -34,11 +35,18 @@ final class IntersectionType extends Type
/**
* @param TypeInterface<Tl> $left_type
* @param TypeInterface<Tr> $right_type
*
* @throws Psl\Exception\InvariantViolationException If $left_type, or $right_type is optional.
*/
public function __construct(
TypeInterface $left_type,
TypeInterface $right_type
) {
Psl\invariant(
!$left_type->isOptional() && !$right_type->isOptional(),
'Optional type must be the outermost.'
);

$this->left_type = $left_type;
$this->right_type = $right_type;
}
Expand Down
8 changes: 8 additions & 0 deletions src/Psl/Type/Internal/IterableType.php
Expand Up @@ -4,6 +4,7 @@

namespace Psl\Type\Internal;

use Psl;
use Psl\Iter;
use Psl\Str;
use Psl\Type;
Expand Down Expand Up @@ -35,11 +36,18 @@ final class IterableType extends Type\Type
/**
* @param Type\TypeInterface<Tk> $key_type
* @param Type\TypeInterface<Tv> $value_type
*
* @throws Psl\Exception\InvariantViolationException If $key_value, or $value_type is optional.
*/
public function __construct(
Type\TypeInterface $key_type,
Type\TypeInterface $value_type
) {
Psl\invariant(
!$key_type->isOptional() && !$value_type->isOptional(),
'Optional type must be the outermost.'
);

$this->key_type = $key_type;
$this->value_type = $value_type;
}
Expand Down
146 changes: 146 additions & 0 deletions src/Psl/Type/Internal/LiteralScalarType.php
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

namespace Psl\Type\Internal;

use Psl\Str;
use Psl\Type;
use Psl\Type\Exception\AssertException;
use Psl\Type\Exception\CoercionException;

/**
* @template T of string|int|float|bool
*
* @extends Type\Type<T>
*
* @internal
*/
final class LiteralScalarType extends Type\Type
{
/**
* @var T
*/
private $value;

/**
* @param T $value
*/
public function __construct($value)
{
$this->value = $value;
}

/**
* @param mixed $value
*
* @psalm-assert-if-true T $value
*/
public function matches($value): bool
{
return $this->value === $value;
}

/**
* @param mixed $value
*
* @throws CoercionException
*
* @return T
*/
public function coerce($value)
{
if ($value === $this->value) {
/** @var T $value */
return $value;
}

/** @psalm-suppress DocblockTypeContradiction */
if (Type\string()->matches($this->value)) {
$coerced_value = Type\string()->withTrace($this->getTrace())->coerce($value);
if ($this->value === $coerced_value) {
/** @var T $coerced_value */
return $coerced_value;
}

throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
}

/** @psalm-suppress DocblockTypeContradiction */
if (Type\int()->matches($this->value)) {
$coerced_value = Type\int()->withTrace($this->getTrace())->coerce($value);
if ($this->value === $coerced_value) {
/** @var T $coerced_value */
return $coerced_value;
}

throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
}

/** @psalm-suppress DocblockTypeContradiction */
if (Type\float()->matches($this->value)) {
$coerced_value = Type\float()->withTrace($this->getTrace())->coerce($value);
if ($this->value === $coerced_value) {
/** @var T $coerced_value */
return $coerced_value;
}

throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
}

/** @psalm-suppress DocblockTypeContradiction */
if (Type\bool()->matches($this->value)) {
$coerced_value = Type\bool()->withTrace($this->getTrace())->coerce($value);
if ($this->value === $coerced_value) {
/** @var T $coerced_value */
return $coerced_value;
}

throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
}

throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
}

/**
* @param mixed $value
*
* @psalm-assert T $value
*
* @throws AssertException
*/
public function assert($value)
{
if ($this->value === $value) {
/** @var T */
return $value;
}

throw AssertException::withValue($value, $this->toString(), $this->getTrace());
}

public function toString(): string
{
/** @var int|string|float|bool $value */
$value = $this->value;
if (Type\string()->matches($value)) {
return Str\format('"%s"', $value);
}

if (Type\int()->matches($value)) {
return Str\format('%d', $value);
}

if (Type\float()->matches($value)) {
$string_representation = Str\trim_right(Str\format('%.14F', $value), '0');
/** @psalm-suppress MissingThrowsDocblock */
if (Str\ends_with($string_representation, '.')) {
$string_representation .= '0';
}

return $string_representation;
}

return $value ? 'true' : 'false';
}
}
8 changes: 8 additions & 0 deletions src/Psl/Type/Internal/MapType.php
Expand Up @@ -4,6 +4,7 @@

namespace Psl\Type\Internal;

use Psl;
use Psl\Collection;
use Psl\Dict;
use Psl\Str;
Expand Down Expand Up @@ -37,11 +38,18 @@ final class MapType extends Type\Type
/**
* @param Type\TypeInterface<Tk> $key_type
* @param Type\TypeInterface<Tv> $value_type
*
* @throws Psl\Exception\InvariantViolationException If $key_value, or $value_type is optional.
*/
public function __construct(
Type\TypeInterface $key_type,
Type\TypeInterface $value_type
) {
Psl\invariant(
!$key_type->isOptional() && !$value_type->isOptional(),
'Optional type must be the outermost.'
);

$this->key_type = $key_type;
$this->value_type = $value_type;
}
Expand Down

0 comments on commit c5ede33

Please sign in to comment.